@nitra/cursor 1.8.142 → 1.8.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mdc/docker.mdc CHANGED
@@ -9,7 +9,7 @@ alwaysApply: false
9
9
 
10
10
  [hadolint](https://github.com/hadolint/hadolint) перевіряє Dockerfile на типові помилки та рекомендації (`FROM`, `RUN`, `COPY`, shell form тощо).
11
11
 
12
- Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginx`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub: **`mirror.gcr.io/oven/bun`**, **`mirror.gcr.io/library/alpine`**, **`mirror.gcr.io/library/nginx`**, **`mirror.gcr.io/library/node`**. Перевіряє **`check-docker.mjs`**, деталі в **`npm/scripts/utils/docker-mirror.mjs`**.
12
+ Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginxinc/nginx-unprivileged`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub: **`mirror.gcr.io/oven/bun`**, **`mirror.gcr.io/library/alpine`**, **`mirror.gcr.io/nginxinc/nginx-unprivileged`**, **`mirror.gcr.io/library/node`**. Перевіряє **`check-docker.mjs`**, деталі в **`npm/scripts/utils/docker-mirror.mjs`**.
13
13
 
14
14
  Також Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
15
15
 
@@ -17,13 +17,13 @@ alwaysApply: false
17
17
  - **ультра-легкі (glibc / одна статична збірка)**: `scratch` — тільки як `FROM scratch` (офіційний порожній базовий шар), коли весь **runtime** уже в `COPY --from=…`
18
18
  - **glibc, Debian (slim)**: `mirror.gcr.io/library/debian:*` **лише** з тегом, у якому є `slim` (наприклад `bookworm-slim`, `trixie-slim`), а не `bookworm` без `slim`
19
19
  - **виняток (інтерпретовані стеки)**: `mirror.gcr.io/library/php:*` або `mirror.gcr.io/library/python:*` — якщо сервіс має крутитися в офіційному runtime PHP чи Python, а не як один бінарник на Alpine; інакше лишай **alpine** у фінальному stage
20
- - **frontend**: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
20
+ - **frontend**: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` або `mirror.gcr.io/openresty/openresty:*`
21
21
 
22
22
  Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) — цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
23
23
 
24
24
  ## Мінімальні образи
25
25
 
26
- Якщо використовується nginx, то намагайся щоб тег був `alpine-slim`
26
+ Якщо використовується nginx, то **використовуй** `mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim` не `latest` / `alpine`).
27
27
 
28
28
  ## компіляція
29
29
 
package/mdc/vue.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Vue
3
3
  alwaysApply: true
4
- version: '1.2'
4
+ version: '1.3'
5
5
  ---
6
6
 
7
7
  # Vue 3 Composition API — правила для .cursorrules
@@ -111,6 +111,7 @@ const additionalInstructions = `
111
111
  - Якість коду: **ESLint** + **eslint-plugin-vue**; форматування — **oxfmt**, див. `text.mdc`.
112
112
  - Збірка та dev-сервер — **Vite**
113
113
  - **Vue Devtools** для дебагу; продакшен-збірка — **`vite build`**, оптимізація асетів і кешування на рівні деплою / CDN.
114
+ - **esbuild заборонено:** у проєкті не має бути залежності `esbuild` і згадок `esbuild` у конфігах/коді. Якщо десь є налаштування або інструкції під `esbuild` — заміни на **rolldown**.
114
115
 
115
116
  ### CI/CD
116
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.142",
3
+ "version": "1.8.144",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -9,8 +9,7 @@
9
9
  * - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
10
10
  * містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
11
11
  * `mirror.gcr.io/library/python:*`
12
- * - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` (preferred) або `mirror.gcr.io/library/nginx:*`,
13
- * `mirror.gcr.io/openresty/openresty:*`
12
+ * - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*`, `mirror.gcr.io/openresty/openresty:*`
14
13
  *
15
14
  * Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
16
15
  * то очікується компіляція в один бінарник через `bun build --compile` у build stage, а у
@@ -19,8 +18,8 @@
19
18
  * Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
20
19
  * дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
21
20
  *
22
- * Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`, `mirror.gcr.io/library/nginx`) у
23
- * будь-якому `FROM` очікується тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
21
+ * Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`) у будь-якому `FROM` очікується
22
+ * тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
24
23
  * `alpine` / інші. `nginx-unprivileged` запускається від не-root користувача (uid=101) без явного
25
24
  * `USER` у Dockerfile — перевірка non-root для нього пропускається.
26
25
  *
@@ -42,7 +41,6 @@ const BUN_BUILD_COMPILE_RE = /\bbun\s+build\b[^\n]*\s--compile\b/iu
42
41
  const BUN_WORD_RE = /\bbun\b/iu
43
42
  const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
44
43
 
45
- const NGINX_MIRROR_PREFIX = 'mirror.gcr.io/library/nginx'
46
44
  const NGINX_UNPRIVILEGED_MIRROR_PREFIX = 'mirror.gcr.io/nginxinc/nginx-unprivileged'
47
45
 
48
46
  /**
@@ -97,7 +95,6 @@ const RUNTIME_IMAGES = /** @type {const} */ ([
97
95
  'mirror.gcr.io/library/alpine',
98
96
  'mirror.gcr.io/library/php',
99
97
  'mirror.gcr.io/library/python',
100
- 'mirror.gcr.io/library/nginx',
101
98
  'mirror.gcr.io/nginxinc/nginx-unprivileged',
102
99
  'mirror.gcr.io/openresty/openresty'
103
100
  ])
@@ -179,7 +176,6 @@ export function getMultistageAndRuntimeHint(fileContent) {
179
176
  * Очікування:
180
177
  * - у build stage є `bun build --compile`;
181
178
  * - у фінальному stage немає викликів `bun` (залишків build tooling).
182
- *
183
179
  * @param {string} fileContent вміст Dockerfile/Containerfile
184
180
  * @returns {string | null} повідомлення помилки або null
185
181
  */
@@ -194,7 +190,6 @@ export function getBunCompileHint(fileContent) {
194
190
  const hasBunInstall = BUN_INSTALL_RE.test(fileContent)
195
191
  const isFinalAlpine = lastLower.startsWith('mirror.gcr.io/library/alpine:')
196
192
  const isFinalFrontend =
197
- lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
198
193
  lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`) ||
199
194
  lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
200
195
 
@@ -216,14 +211,13 @@ export function getBunCompileHint(fileContent) {
216
211
  }
217
212
 
218
213
  /**
219
- * Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged` або
220
- * `mirror.gcr.io/library/nginx`) у `FROM` вказано тег `alpine-slim` (docker.mdc).
221
- *
214
+ * Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`) у `FROM` вказано
215
+ * тег `alpine-slim` (docker.mdc).
222
216
  * @param {string} fileContent вміст Dockerfile/Containerfile
223
217
  * @returns {string | null} повідомлення помилки або null
224
218
  */
225
219
  export function getNginxAlpineSlimTagHint(fileContent) {
226
- const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX, NGINX_MIRROR_PREFIX]
220
+ const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX]
227
221
  for (const { line, image } of parseFromStages(fileContent)) {
228
222
  const noDigest = (image.split('@')[0] || '').trim()
229
223
  const d = noDigest.toLowerCase()
@@ -247,7 +241,6 @@ export function getNginxAlpineSlimTagHint(fileContent) {
247
241
  * Очікування:
248
242
  * - у фінальному stage має бути інструкція `USER <name|uid>`;
249
243
  * - користувач не має бути `root` і не має бути `0`.
250
- *
251
244
  * @param {string} fileContent вміст Dockerfile/Containerfile
252
245
  * @returns {string | null} повідомлення помилки або null
253
246
  */
@@ -289,7 +282,7 @@ export function getNonRootRuntimeHint(fileContent) {
289
282
  */
290
283
  export async function check() {
291
284
  const reporter = createCheckReporter()
292
- const { pass, fail } = reporter
285
+ const { pass } = reporter
293
286
 
294
287
  const root = process.cwd()
295
288
  const files = await findDockerfilePaths(root)
@@ -302,42 +295,45 @@ export async function check() {
302
295
  pass(`Знайдено файлів для hadolint: ${files.length}`)
303
296
 
304
297
  for (const abs of files) {
305
- const rel = posixRel(root, abs) || basename(abs)
306
- const content = await readFile(abs, 'utf8')
307
- const hint = getMirrorGcrHint(content)
308
- if (hint) {
309
- fail(`${rel} (mirror.gcr.io): ${hint}`)
310
- }
298
+ await checkDockerfile(reporter, root, abs)
299
+ }
311
300
 
312
- const multistageHint = getMultistageAndRuntimeHint(content)
313
- if (multistageHint) {
314
- fail(`${rel} (multistage): ${multistageHint}`)
315
- }
301
+ return reporter.getExitCode()
302
+ }
316
303
 
317
- const compileHint = getBunCompileHint(content)
318
- if (compileHint) {
319
- fail(`${rel} (compile): ${compileHint}`)
320
- }
304
+ /**
305
+ * Перевіряє один Dockerfile/Containerfile: mirror.gcr.io, multistage/runtime, compile/non-root/nginx tag і hadolint.
306
+ * @param {ReturnType<typeof createCheckReporter>} reporter репортер перевірок
307
+ * @param {string} root корінь репозиторію
308
+ * @param {string} abs абсолютний шлях до Dockerfile/Containerfile
309
+ * @returns {Promise<void>}
310
+ */
311
+ async function checkDockerfile(reporter, root, abs) {
312
+ const { pass, fail } = reporter
313
+ const rel = posixRel(root, abs) || basename(abs)
314
+ const content = await readFile(abs, 'utf8')
321
315
 
322
- const nonRootHint = getNonRootRuntimeHint(content)
323
- if (nonRootHint) {
324
- fail(`${rel} (non-root): ${nonRootHint}`)
325
- }
316
+ const hint = getMirrorGcrHint(content)
317
+ if (hint) fail(`${rel} (mirror.gcr.io): ${hint}`)
326
318
 
327
- const nginxSlimHint = getNginxAlpineSlimTagHint(content)
328
- if (nginxSlimHint) {
329
- fail(`${rel} (nginx tag): ${nginxSlimHint}`)
330
- }
319
+ const multistageHint = getMultistageAndRuntimeHint(content)
320
+ if (multistageHint) fail(`${rel} (multistage): ${multistageHint}`)
331
321
 
332
- const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
333
- const tail = (stdout + stderr).trim()
334
- if (ok) {
335
- pass(`${rel} (${via})`)
336
- } else {
337
- const detail = tail ? `:\n${tail}` : ''
338
- fail(`${rel} (${via})${detail}`)
339
- }
340
- }
322
+ const compileHint = getBunCompileHint(content)
323
+ if (compileHint) fail(`${rel} (compile): ${compileHint}`)
341
324
 
342
- return reporter.getExitCode()
325
+ const nonRootHint = getNonRootRuntimeHint(content)
326
+ if (nonRootHint) fail(`${rel} (non-root): ${nonRootHint}`)
327
+
328
+ const nginxSlimHint = getNginxAlpineSlimTagHint(content)
329
+ if (nginxSlimHint) fail(`${rel} (nginx tag): ${nginxSlimHint}`)
330
+
331
+ const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
332
+ const tail = (stdout + stderr).trim()
333
+ if (ok) {
334
+ pass(`${rel} (${via})`)
335
+ } else {
336
+ const detail = tail ? `:\n${tail}` : ''
337
+ fail(`${rel} (${via})${detail}`)
338
+ }
343
339
  }
@@ -24,6 +24,98 @@ import { walkDir } from './utils/walkDir.mjs'
24
24
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
25
25
 
26
26
  const MAJOR_VERSION_RE = /(\d+)/
27
+ const ESBUILD_RE = /\besbuild\b/
28
+
29
+ /**
30
+ * Визначає, чи можна сканувати файл як текст на згадки `esbuild`.
31
+ * @param {string} relPosix відносний шлях у posix-форматі
32
+ * @returns {boolean} true якщо файл варто перевірити
33
+ */
34
+ function isEsbuildScanFile(relPosix) {
35
+ if (
36
+ relPosix.startsWith('node_modules/') ||
37
+ relPosix.startsWith('dist/') ||
38
+ relPosix.startsWith('build/') ||
39
+ relPosix.startsWith('coverage/') ||
40
+ relPosix.startsWith('.git/')
41
+ ) {
42
+ return false
43
+ }
44
+
45
+ const lower = relPosix.toLowerCase()
46
+ if (
47
+ lower === 'bun.lock' ||
48
+ lower === 'bun.lockb' ||
49
+ lower === 'package-lock.json' ||
50
+ lower === 'yarn.lock' ||
51
+ lower === 'pnpm-lock.yaml'
52
+ ) {
53
+ return false
54
+ }
55
+
56
+ return (
57
+ lower.endsWith('.js') ||
58
+ lower.endsWith('.mjs') ||
59
+ lower.endsWith('.cjs') ||
60
+ lower.endsWith('.ts') ||
61
+ lower.endsWith('.tsx') ||
62
+ lower.endsWith('.vue') ||
63
+ lower.endsWith('.json') ||
64
+ lower.endsWith('.jsonc') ||
65
+ lower.endsWith('.yaml') ||
66
+ lower.endsWith('.yml') ||
67
+ lower.endsWith('.md') ||
68
+ lower.endsWith('.mdc')
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Сканує дерево пакета на згадки `esbuild` і підказує заміну на `rolldown`.
74
+ * @param {string} rootDir відносний шлях до пакета
75
+ * @param {string} absPackageRoot абсолютний шлях до кореня пакета
76
+ * @param {string} prefix параметр prefix
77
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
78
+ * @param {(msg: string) => void} fail callback при помилці
79
+ */
80
+ async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fail) {
81
+ /** @type {{ rel: string; line: number; snippet: string }[]} */
82
+ const hits = []
83
+
84
+ await walkDir(absPackageRoot, absPath => {
85
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
86
+ if (!isEsbuildScanFile(rel)) return
87
+ hits.push({ rel, line: 0, snippet: '' })
88
+ })
89
+
90
+ // ми використали hits як буфер шляхів; зараз перетворимо на реальні співпадіння
91
+ /** @type {{ rel: string; line: number; snippet: string }[]} */
92
+ const matches = []
93
+ for (const { rel } of hits) {
94
+ const content = await readFile(join(absPackageRoot, rel), 'utf8')
95
+ if (!ESBUILD_RE.test(content)) continue
96
+
97
+ const lines = content.split('\n')
98
+ for (let i = 0; i < lines.length; i++) {
99
+ if (ESBUILD_RE.test(lines[i])) {
100
+ matches.push({ rel, line: i + 1, snippet: lines[i].trim() })
101
+ if (matches.length >= 30) break
102
+ }
103
+ }
104
+ if (matches.length >= 30) break
105
+ }
106
+
107
+ if (matches.length === 0) {
108
+ passFn(`${prefix}немає згадок 'esbuild' у джерелах пакета (очікується rolldown)`)
109
+ return
110
+ }
111
+
112
+ for (const m of matches) {
113
+ fail(`${prefix}${m.rel}:${m.line} — знайдено 'esbuild'. Замінити на 'rolldown'. Фрагмент: ${m.snippet}`)
114
+ }
115
+ if (matches.length >= 30) {
116
+ fail(`${prefix}показано перші 30 збігів 'esbuild' (замінити на 'rolldown')`)
117
+ }
118
+ }
27
119
 
28
120
  /**
29
121
  * Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
@@ -107,6 +199,9 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
107
199
  return
108
200
  }
109
201
  const content = await readFile(join(rootDir, viteConfig), 'utf8')
202
+ if (ESBUILD_RE.test(content)) {
203
+ fail(`${prefix}${viteConfig} містить 'esbuild' — заміни на 'rolldown'`)
204
+ }
110
205
  const checks = [
111
206
  { token: 'VueMacros', ok: `${viteConfig} використовує VueMacros`, err: `${viteConfig} не містить VueMacros` },
112
207
  { token: 'AutoImport', ok: `${viteConfig} використовує AutoImport`, err: `${viteConfig} не містить AutoImport` }
@@ -175,6 +270,10 @@ async function checkVuePackage(rootDir, fail, passFn) {
175
270
  const devDeps = pkg.devDependencies || {}
176
271
  const allDeps = { ...deps, ...devDeps }
177
272
 
273
+ if (allDeps.esbuild) {
274
+ fail(`${prefix}esbuild заборонено (знайдено: ${allDeps.esbuild}). Замінити на rolldown та прибрати esbuild.`)
275
+ }
276
+
178
277
  checkRequiredDep(deps, 'vue', prefix, passFn, fail, 'vue відсутній в dependencies')
179
278
  checkViteVersion(devDeps, prefix, passFn, fail)
180
279
  checkRequiredDep(
@@ -205,6 +304,7 @@ async function checkVuePackage(rootDir, fail, passFn) {
205
304
 
206
305
  await checkViteConfig(rootDir, prefix, passFn, fail)
207
306
  await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
307
+ await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
208
308
  }
209
309
 
210
310
  /**
@@ -215,17 +315,6 @@ export async function check() {
215
315
  const reporter = createCheckReporter()
216
316
  const { pass, fail } = reporter
217
317
 
218
- if (existsSync('.vscode/extensions.json')) {
219
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
220
- if (ext.recommendations?.includes('Vue.volar')) {
221
- pass('extensions.json містить Vue.volar')
222
- } else {
223
- fail('extensions.json не містить Vue.volar — додай до recommendations')
224
- }
225
- } else {
226
- fail('.vscode/extensions.json не існує')
227
- }
228
-
229
318
  const roots = await getMonorepoPackageRootDirs()
230
319
  /** @type {string[]} */
231
320
  const vueRoots = []
@@ -237,8 +326,23 @@ export async function check() {
237
326
  }
238
327
  }
239
328
 
329
+ if (vueRoots.length > 0) {
330
+ if (existsSync('.vscode/extensions.json')) {
331
+ const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
332
+ if (ext.recommendations?.includes('Vue.volar')) {
333
+ pass('extensions.json містить Vue.volar')
334
+ } else {
335
+ fail('extensions.json не містить Vue.volar — додай до recommendations')
336
+ }
337
+ } else {
338
+ fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
339
+ }
340
+ } else {
341
+ pass('Vue.volar: пропущено (у repo немає пакетів з vue у dependencies)')
342
+ }
343
+
240
344
  if (vueRoots.length === 0) {
241
- fail('vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)')
345
+ pass('vue не знайдено в dependencies жодного пакета (перевірка vue пропущена)')
242
346
  return reporter.getExitCode()
243
347
  }
244
348