@nitra/cursor 1.8.104 → 1.8.105

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.
@@ -21,6 +21,15 @@ import { findDockerfilePaths } from './check-docker.mjs'
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
  import { walkDir } from './utils/walkDir.mjs'
23
23
 
24
+ const LINE_SPLIT_RE = /\r?\n/u
25
+ const INI_KEY_RE = /^([A-Za-z_]\w*)\s*=/u
26
+ const RETURN_200_RE = /return\s+200/u
27
+ const GZIP_STATIC_ON_RE = /gzip_static\s+on/gu
28
+ const PROXY_LIKE_RE = /\b(proxy_pass|proxy_redirect|proxy_set_header|proxy_http_version|fastcgi_pass|grpc_pass|uwsgi_pass)\b/u
29
+ const FIND_CMD_RE = /\bfind\b/u
30
+ const GZIP_CMD_RE = /\bgzip\b/u
31
+ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
32
+
24
33
  /**
25
34
  * Збирає абсолютні шляхи до **default.conf.template** у репозиторії; шлях `tests/fixtures` не обходиться як проєктний шаблон.
26
35
  * @param {string} root корінь cwd
@@ -82,10 +91,10 @@ export async function migrateDefaultTplConfFiles(root) {
82
91
  export function parseIniVariableNames(iniText) {
83
92
  /** @type {string[]} */
84
93
  const keys = []
85
- for (const line of iniText.split(/\r?\n/u)) {
94
+ for (const line of iniText.split(LINE_SPLIT_RE)) {
86
95
  const t = line.trim()
87
96
  if (t !== '' && !t.startsWith('#') && !t.startsWith(';')) {
88
- const m = t.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/u)
97
+ const m = t.match(INI_KEY_RE)
89
98
  if (m) keys.push(m[1])
90
99
  }
91
100
  }
@@ -111,7 +120,7 @@ export function nginxTemplateViolations(content) {
111
120
  { msg: 'відсутнє root /usr/share/nginx/html', ok: c => c.includes('root /usr/share/nginx/html') },
112
121
  {
113
122
  msg: 'location /healthz має повертати healthy (див. nginx-default-tpl.mdc)',
114
- ok: c => c.includes('/healthz') && (c.includes('healthy') || /return\s+200/u.test(c))
123
+ ok: c => c.includes('/healthz') && (c.includes('healthy') || RETURN_200_RE.test(c))
115
124
  },
116
125
  {
117
126
  msg: 'відсутній location для статики без gzip (gif|jpeg|png|ico|woff2|xlsx) з Cache-Control 31536000',
@@ -126,7 +135,7 @@ export function nginxTemplateViolations(content) {
126
135
  },
127
136
  {
128
137
  msg: 'gzip_static on має бути принаймні двічі (два location зі стисненням)',
129
- ok: c => (c.match(/gzip_static\s+on/gu) ?? []).length >= 2
138
+ ok: c => (c.match(GZIP_STATIC_ON_RE) ?? []).length >= 2
130
139
  },
131
140
  { msg: 'відсутнє використання $PUBLIC_PATH у location', ok: c => c.includes('$PUBLIC_PATH') },
132
141
  {
@@ -144,9 +153,7 @@ export function nginxTemplateViolations(content) {
144
153
  }
145
154
 
146
155
  // cspell:ignore fastcgi uwsgi
147
- const proxyLike =
148
- /\b(proxy_pass|proxy_redirect|proxy_set_header|proxy_http_version|fastcgi_pass|grpc_pass|uwsgi_pass)\b/u
149
- if (proxyLike.test(content)) {
156
+ if (PROXY_LIKE_RE.test(content)) {
150
157
  return 'знайдено proxy, gRPC або інший *_pass до бекенду — прибери з шаблону, логіку винеси в HTTPRoute (k8s) (див. nginx-default-tpl.mdc)'
151
158
  }
152
159
 
@@ -242,11 +249,11 @@ export function iniKeysMissingInTemplate(keys, template) {
242
249
  function dockerfileHasGzipStaticPipeline(dockerfileContent) {
243
250
  const c = dockerfileContent
244
251
  return (
245
- /\bfind\b/u.test(c) &&
252
+ FIND_CMD_RE.test(c) &&
246
253
  c.includes('/usr/share/nginx/html') &&
247
- /\bgzip\b/u.test(c) &&
254
+ GZIP_CMD_RE.test(c) &&
248
255
  c.includes('-k') &&
249
- /\*\.(?:js|css)/u.test(c)
256
+ GZIP_EXTENSION_RE.test(c)
250
257
  )
251
258
  }
252
259
 
@@ -16,8 +16,6 @@ import { readFile, stat } from 'node:fs/promises'
16
16
  import { join } from 'node:path'
17
17
 
18
18
  import { createCheckReporter } from './utils/check-reporter.mjs'
19
-
20
- const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
21
19
  import {
22
20
  hasIdTokenWritePermission,
23
21
  hasNpmPublishStepWithPackage,
@@ -27,6 +25,8 @@ import {
27
25
  } from './utils/gha-workflow.mjs'
28
26
  import { walkDir } from './utils/walkDir.mjs'
29
27
 
28
+ const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
29
+
30
30
  /** Канонічний entrypoint типів для пакетів із вихідним `.js` під каталогом `npm/src` */
31
31
  const TYPES_INDEX = './types/index.d.ts'
32
32
 
@@ -195,7 +195,7 @@ export async function check() {
195
195
  fail(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
196
196
  }
197
197
  } else {
198
- if (typeof typesField === 'string' && /^\.\/types\/.+\.d\.(ts|mts)$/.test(typesField)) {
198
+ if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
199
199
  pass(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
200
200
  } else {
201
201
  fail(
@@ -178,9 +178,7 @@ export async function check() {
178
178
  )
179
179
  }
180
180
  } else {
181
- fail(
182
- `.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`
183
- )
181
+ fail(`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`)
184
182
  }
185
183
  } else {
186
184
  fail('.oxfmtrc.json не існує — створи його')
@@ -12,8 +12,6 @@ import { readFile } from 'node:fs/promises'
12
12
  import { join, relative } from 'node:path'
13
13
 
14
14
  import { createCheckReporter } from './utils/check-reporter.mjs'
15
-
16
- const MAJOR_VERSION_RE = /(\d+)/
17
15
  import {
18
16
  findForbiddenVueImportsInSourceFile,
19
17
  isVueImportScanSourceFile,
@@ -22,6 +20,8 @@ import {
22
20
  import { walkDir } from './utils/walkDir.mjs'
23
21
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
24
22
 
23
+ const MAJOR_VERSION_RE = /(\d+)/
24
+
25
25
  /**
26
26
  * Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
27
27
  * @param {string} rootDir відносний шлях (`'.'` або `site` тощо)
@@ -59,11 +59,15 @@ export function lintDockerfileWithHadolint(root, absPath) {
59
59
  }
60
60
  }
61
61
 
62
- const docker = spawnSync(dockerPath, ['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel], {
63
- cwd: root,
64
- encoding: 'utf8',
65
- maxBuffer: 10 * 1024 * 1024
66
- })
62
+ const docker = spawnSync(
63
+ dockerPath,
64
+ ['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel],
65
+ {
66
+ cwd: root,
67
+ encoding: 'utf8',
68
+ maxBuffer: 10 * 1024 * 1024
69
+ }
70
+ )
67
71
  if (docker.error) {
68
72
  return {
69
73
  ok: false,
@@ -6,6 +6,11 @@
6
6
  */
7
7
  import { parse } from 'yaml'
8
8
 
9
+ const CHECKOUT_USES_MARKER = 'actions/checkout@'
10
+ const CHECKOUT_V6_USES = 'actions/checkout@v6'
11
+ const LOCAL_SETUP_BUN_DEPS_MARKER = './.github/actions/setup-bun-deps'
12
+ const BUNX_OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
13
+
9
14
  /**
10
15
  * Парсить workflow YAML у звичайний об’єкт; при синтаксичній помилці — `null`.
11
16
  * @param {string} content вміст файлу
@@ -26,26 +31,12 @@ export function parseWorkflowYaml(content) {
26
31
  * @returns {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} плоский список кроків з метаданими
27
32
  */
28
33
  export function flattenWorkflowSteps(root) {
29
- const jobs = root?.jobs
30
- if (!jobs || typeof jobs !== 'object') {
31
- return []
32
- }
33
34
  /** @type {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} */
34
35
  const out = []
35
- for (const [jobId, job] of Object.entries(jobs)) {
36
- if (job && typeof job === 'object') {
37
- const steps = /** @type {{ steps?: unknown }} */ (job).steps
38
- if (Array.isArray(steps)) {
39
- for (const [stepIndex, step] of steps.entries()) {
40
- if (step && typeof step === 'object') {
41
- out.push({
42
- jobId,
43
- stepIndex,
44
- step: /** @type {Record<string, unknown>} */ (step)
45
- })
46
- }
47
- }
48
- }
36
+ for (const [jobId, job] of workflowJobsEntries(root)) {
37
+ const steps = workflowJobSteps(job)
38
+ for (const [stepIndex, step] of steps.entries()) {
39
+ out.push({ jobId, stepIndex, step })
49
40
  }
50
41
  }
51
42
  return out
@@ -100,37 +91,15 @@ export function hasAnyStepUsesContaining(root, substrings) {
100
91
  * @returns {boolean} `false`, якщо є setup без попереднього checkout
101
92
  */
102
93
  export function hasCheckoutBeforeLocalSetupBunDeps(root, setupPathSubstrings) {
103
- const jobs = root?.jobs
104
- if (!jobs || typeof jobs !== 'object') {
105
- return true
106
- }
107
- for (const job of Object.values(jobs)) {
108
- if (job && typeof job === 'object') {
109
- const steps = /** @type {{ steps?: unknown }} */ (job).steps
110
- if (Array.isArray(steps)) {
111
- for (let i = 0; i < steps.length; i++) {
112
- const step = steps[i]
113
- if (step && typeof step === 'object') {
114
- const uses = getStepUses(/** @type {Record<string, unknown>} */ (step))
115
- const isSetup = setupPathSubstrings.some(s => uses.includes(s))
116
- if (isSetup) {
117
- let foundCheckout = false
118
- for (let j = 0; j < i; j++) {
119
- const prev = steps[j]
120
- if (prev && typeof prev === 'object') {
121
- const u = getStepUses(/** @type {Record<string, unknown>} */ (prev))
122
- if (u.includes('actions/checkout@')) {
123
- foundCheckout = true
124
- break
125
- }
126
- }
127
- }
128
- if (!foundCheckout) {
129
- return false
130
- }
131
- }
132
- }
133
- }
94
+ for (const [, job] of workflowJobsEntries(root)) {
95
+ let hasCheckoutStep = false
96
+ for (const step of workflowJobSteps(job)) {
97
+ const uses = getStepUses(step)
98
+ if (uses.includes(CHECKOUT_USES_MARKER)) {
99
+ hasCheckoutStep = true
100
+ }
101
+ if (setupPathSubstrings.some(s => uses.includes(s)) && !hasCheckoutStep) {
102
+ return false
134
103
  }
135
104
  }
136
105
  }
@@ -279,26 +248,15 @@ export function verifyLintJsWorkflowStructure(root) {
279
248
  const usesList = steps.map(s => getStepUses(s.step))
280
249
  const runBlob = steps.map(s => getStepRun(s.step)).join('\n')
281
250
 
282
- if (!usesList.some(u => u.includes('actions/checkout@v6'))) {
251
+ if (!usesList.some(u => u.includes(CHECKOUT_V6_USES))) {
283
252
  failures.push('немає кроку uses: actions/checkout@v6')
284
253
  }
285
254
 
286
- let checkoutOk = false
287
- for (const { step } of steps) {
288
- const u = getStepUses(step)
289
- if (u.includes('actions/checkout@v6')) {
290
- const w = step.with
291
- if (w && typeof w === 'object' && /** @type {Record<string, unknown>} */ (w)['persist-credentials'] === false) {
292
- checkoutOk = true
293
- break
294
- }
295
- }
296
- }
297
- if (!checkoutOk) {
255
+ if (!hasCheckoutWithPersistCredentialsFalse(steps)) {
298
256
  failures.push('checkout@v6 без with.persist-credentials: false')
299
257
  }
300
258
 
301
- if (!usesList.some(u => u.includes('./.github/actions/setup-bun-deps'))) {
259
+ if (!usesList.some(u => u.includes(LOCAL_SETUP_BUN_DEPS_MARKER))) {
302
260
  failures.push('немає uses: ./.github/actions/setup-bun-deps')
303
261
  }
304
262
 
@@ -312,15 +270,7 @@ export function verifyLintJsWorkflowStructure(root) {
312
270
  failures.push('у run немає bunx jscpd .')
313
271
  }
314
272
 
315
- for (const { step } of steps) {
316
- const run = getStepRun(step)
317
- if (/bunx\s+oxlint[^\n]*--fix/u.test(run)) {
318
- failures.push('у run є oxlint з --fix (у CI заборонено)')
319
- }
320
- if (run.includes('eslint --fix')) {
321
- failures.push('у run є eslint --fix (у CI заборонено)')
322
- }
323
- }
273
+ appendCiFixFlagFailures(failures, steps)
324
274
 
325
275
  return failures.length === 0 ? { ok: true, failures: [] } : { ok: false, failures }
326
276
  }
@@ -348,3 +298,71 @@ export function anyRunStepIncludes(root, needle) {
348
298
  export function anyRunStepIncludesStylelint(root) {
349
299
  return anyRunStepIncludes(root, 'npx stylelint')
350
300
  }
301
+
302
+ /**
303
+ * Повертає jobs як список пар [jobId, job], якщо структура валідна.
304
+ * @param {Record<string, unknown>} root корінь workflow
305
+ * @returns {[string, Record<string, unknown>][]} список jobs
306
+ */
307
+ function workflowJobsEntries(root) {
308
+ const jobs = root?.jobs
309
+ if (!jobs || typeof jobs !== 'object') {
310
+ return []
311
+ }
312
+ return Object.entries(jobs).flatMap(([jobId, job]) =>
313
+ job && typeof job === 'object' ? [[jobId, /** @type {Record<string, unknown>} */ (job)]] : []
314
+ )
315
+ }
316
+
317
+ /**
318
+ * Повертає валідні кроки job.
319
+ * @param {Record<string, unknown>} job job-об’єкт
320
+ * @returns {Record<string, unknown>[]} кроки job
321
+ */
322
+ function workflowJobSteps(job) {
323
+ const steps = /** @type {{ steps?: unknown }} */ (job).steps
324
+ if (!Array.isArray(steps)) {
325
+ return []
326
+ }
327
+ return steps.flatMap(step => (step && typeof step === 'object' ? [/** @type {Record<string, unknown>} */ (step)] : []))
328
+ }
329
+
330
+ /**
331
+ * Чи є checkout@v6 з `persist-credentials: false`.
332
+ * @param {{ step: Record<string, unknown> }[]} steps кроки flattenWorkflowSteps
333
+ * @returns {boolean} true, якщо знайдено очікуваний checkout
334
+ */
335
+ function hasCheckoutWithPersistCredentialsFalse(steps) {
336
+ for (const { step } of steps) {
337
+ const uses = getStepUses(step)
338
+ if (uses.includes(CHECKOUT_V6_USES)) {
339
+ const withObj = step.with
340
+ if (
341
+ withObj &&
342
+ typeof withObj === 'object' &&
343
+ /** @type {Record<string, unknown>} */ (withObj)['persist-credentials'] === false
344
+ ) {
345
+ return true
346
+ }
347
+ }
348
+ }
349
+ return false
350
+ }
351
+
352
+ /**
353
+ * Додає порушення для `--fix` у CI-кроках lint-js workflow.
354
+ * @param {string[]} failures акумулятор порушень
355
+ * @param {{ step: Record<string, unknown> }[]} steps кроки flattenWorkflowSteps
356
+ * @returns {void}
357
+ */
358
+ function appendCiFixFlagFailures(failures, steps) {
359
+ for (const { step } of steps) {
360
+ const run = getStepRun(step)
361
+ if (BUNX_OXLINT_FIX_RE.test(run)) {
362
+ failures.push('у run є oxlint з --fix (у CI заборонено)')
363
+ }
364
+ if (run.includes('eslint --fix')) {
365
+ failures.push('у run є eslint --fix (у CI заборонено)')
366
+ }
367
+ }
368
+ }
@@ -10,6 +10,43 @@ import { dirname, join, relative } from 'node:path'
10
10
 
11
11
  const TRAILING_SLASH_RE = /\/$/
12
12
 
13
+ /**
14
+ * Нормалізує workspace-патерн до POSIX-формату і прибирає хвостові `/`.
15
+ * @param {string} pattern сирий workspace-патерн
16
+ * @returns {string} нормалізований патерн або `.`
17
+ */
18
+ function normalizeWorkspacePattern(pattern) {
19
+ let normalized = pattern.replaceAll('\\', '/')
20
+ while (TRAILING_SLASH_RE.test(normalized)) {
21
+ normalized = normalized.slice(0, -1)
22
+ }
23
+ return normalized || '.'
24
+ }
25
+
26
+ /**
27
+ * Додає каталоги пакетів до set за workspace-патерном.
28
+ * @param {Set<string>} roots set коренів пакетів
29
+ * @param {string} repoRoot корінь репозиторію
30
+ * @param {string} workspacePattern нормалізований workspace-патерн
31
+ * @returns {Promise<void>}
32
+ */
33
+ async function addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern) {
34
+ if (workspacePattern.includes('*')) {
35
+ const globPat = `${workspacePattern}/package.json`
36
+ for await (const relPkgJsonPath of glob(globPat, { cwd: repoRoot })) {
37
+ const absPkgJsonPath = join(repoRoot, relPkgJsonPath)
38
+ const relRoot = relative(repoRoot, dirname(absPkgJsonPath))
39
+ roots.add(relRoot === '' ? '.' : relRoot)
40
+ }
41
+ return
42
+ }
43
+
44
+ const pkgJsonPath = join(repoRoot, workspacePattern, 'package.json')
45
+ if (existsSync(pkgJsonPath)) {
46
+ roots.add(workspacePattern)
47
+ }
48
+ }
49
+
13
50
  /**
14
51
  * Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
15
52
  * @param {unknown} workspaces значення `workspaces` з кореневого package.json
@@ -37,22 +74,8 @@ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
37
74
  }
38
75
  const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
39
76
  for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
40
- let w = raw.replaceAll('\\', '/')
41
- while (TRAILING_SLASH_RE.test(w)) {
42
- w = w.slice(0, -1)
43
- }
44
- w = w || '.'
45
- if (w.includes('*')) {
46
- const globPat = `${w}/package.json`
47
- for await (const f of glob(globPat, { cwd: repoRoot })) {
48
- const abs = join(repoRoot, f)
49
- const rel = relative(repoRoot, dirname(abs))
50
- roots.add(rel === '' ? '.' : rel)
51
- }
52
- } else {
53
- const pkgJson = join(repoRoot, w, 'package.json')
54
- if (existsSync(pkgJson)) roots.add(w)
55
- }
77
+ const workspacePattern = normalizeWorkspacePattern(raw)
78
+ await addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern)
56
79
  }
57
80
  const list = [...roots]
58
81
  list.sort((a, b) => {