@nitra/cursor 1.8.105 → 1.8.106

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.
@@ -25,7 +25,8 @@ const LINE_SPLIT_RE = /\r?\n/u
25
25
  const INI_KEY_RE = /^([A-Za-z_]\w*)\s*=/u
26
26
  const RETURN_200_RE = /return\s+200/u
27
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
28
+ const PROXY_LIKE_RE =
29
+ /\b(proxy_pass|proxy_redirect|proxy_set_header|proxy_http_version|fastcgi_pass|grpc_pass|uwsgi_pass)\b/u
29
30
  const FIND_CMD_RE = /\bfind\b/u
30
31
  const GZIP_CMD_RE = /\bgzip\b/u
31
32
  const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
@@ -266,6 +267,110 @@ function dockerfileHasEnvsSubstTemplate(dockerfileContent) {
266
267
  return dockerfileContent.includes('envsubst') && dockerfileContent.includes('default.conf.template')
267
268
  }
268
269
 
270
+ /**
271
+ * Перевіряє один template-файл і поруч *.ini.
272
+ * @param {string} abs абсолютний шлях до template
273
+ * @param {string} root cwd
274
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
275
+ * @param {(msg: string) => void} failFn callback при помилці
276
+ */
277
+ async function checkTemplateFile(abs, root, passFn, failFn) {
278
+ const rel = relative(root, abs) || abs
279
+ const content = await readFile(abs, 'utf8')
280
+ const v = nginxTemplateViolations(content)
281
+ if (v) {
282
+ failFn(`${rel}: ${v}`)
283
+ } else {
284
+ passFn(`${rel}: структура шаблону узгоджена з nginx-default-tpl.mdc`)
285
+ }
286
+
287
+ const dir = dirname(abs)
288
+ let iniNames = []
289
+ try {
290
+ const dirEntries = await readdir(dir)
291
+ iniNames = dirEntries.filter(n => n.endsWith('.ini'))
292
+ } catch {
293
+ iniNames = []
294
+ }
295
+ if (iniNames.length === 0) {
296
+ failFn(`${rel}: поруч немає жодного *.ini — додай values-*.ini для середовищ (див. nginx-default-tpl.mdc)`)
297
+ return
298
+ }
299
+ passFn(`${rel}: поруч є *.ini (${iniNames.length})`)
300
+ for (const iniName of iniNames) {
301
+ const iniPath = `${dir}/${iniName}`
302
+ const iniRel = relative(root, iniPath) || iniPath
303
+ try {
304
+ const iniRaw = await readFile(iniPath, 'utf8')
305
+ const miss = iniKeysMissingInTemplate(parseIniVariableNames(iniRaw), content)
306
+ if (miss) failFn(`${iniRel}: ${miss}`)
307
+ } catch (error) {
308
+ failFn(`${iniRel}: не вдалося прочитати (${error instanceof Error ? error.message : String(error)})`)
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Перевіряє Dockerfile на наявність gzip та envsubst.
315
+ * @param {string} root корінь репозиторію
316
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
317
+ * @param {(msg: string) => void} failFn callback при помилці
318
+ */
319
+ async function checkDockerfiles(root, passFn, failFn) {
320
+ const dockerPaths = await findDockerfilePaths(root)
321
+ if (dockerPaths.length === 0) {
322
+ failFn(
323
+ 'Є default.conf.template, але немає Dockerfile / Containerfile — додай gzip для статики та envsubst (див. nginx-default-tpl.mdc)'
324
+ )
325
+ return
326
+ }
327
+ const bodies = await Promise.all(dockerPaths.map(p => readFile(p, 'utf8')))
328
+ if (bodies.some(body => dockerfileHasGzipStaticPipeline(body))) {
329
+ passFn('Dockerfile: знайдено крок стиснення статики (find + gzip -k)')
330
+ } else {
331
+ failFn('Dockerfile: потрібен RUN find … /usr/share/nginx/html … gzip -k (див. nginx-default-tpl.mdc)')
332
+ }
333
+ if (bodies.some(body => dockerfileHasEnvsSubstTemplate(body))) {
334
+ passFn('Dockerfile: знайдено envsubst для default.conf.template')
335
+ } else {
336
+ failFn('Dockerfile: потрібен envsubst з default.conf.template (див. nginx-default-tpl.mdc)')
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Перевіряє VSCode extensions.json та settings.json для nginx.
342
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
343
+ * @param {(msg: string) => void} failFn callback при помилці
344
+ */
345
+ async function checkVscodeNginx(passFn, failFn) {
346
+ if (existsSync('.vscode/extensions.json')) {
347
+ const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
348
+ if (ext.recommendations?.includes('ahmadalli.vscode-nginx-conf')) {
349
+ passFn('extensions.json містить ahmadalli.vscode-nginx-conf')
350
+ } else {
351
+ failFn('extensions.json не містить ahmadalli.vscode-nginx-conf')
352
+ }
353
+ } else {
354
+ failFn('Очікується .vscode/extensions.json з ahmadalli.vscode-nginx-conf (див. nginx-default-tpl.mdc)')
355
+ }
356
+
357
+ if (!existsSync('.vscode/settings.json')) {
358
+ failFn('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
359
+ return
360
+ }
361
+ const s = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
362
+ if (s['editor.formatOnSave'] === true) {
363
+ passFn('settings.json: editor.formatOnSave увімкнено')
364
+ } else {
365
+ failFn('settings.json: увімкни editor.formatOnSave: true (див. nginx-default-tpl.mdc)')
366
+ }
367
+ if (s['[nginx]']?.['editor.defaultFormatter'] === 'ahmadalli.vscode-nginx-conf') {
368
+ passFn('settings.json: [nginx] defaultFormatter налаштовано')
369
+ } else {
370
+ failFn('settings.json: [nginx].editor.defaultFormatter має бути ahmadalli.vscode-nginx-conf')
371
+ }
372
+ }
373
+
269
374
  /**
270
375
  * Перевіряє відповідність проєкту правилам nginx-default-tpl.mdc
271
376
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -294,98 +399,11 @@ export async function check() {
294
399
  pass(`Знайдено default.conf.template: ${templates.length}`)
295
400
 
296
401
  for (const abs of templates) {
297
- const rel = relative(root, abs) || abs
298
- const content = await readFile(abs, 'utf8')
299
- const v = nginxTemplateViolations(content)
300
- if (v) {
301
- fail(`${rel}: ${v}`)
302
- } else {
303
- pass(`${rel}: структура шаблону узгоджена з nginx-default-tpl.mdc`)
304
- }
305
-
306
- const dir = dirname(abs)
307
- let iniNames = []
308
- try {
309
- const dirEntries = await readdir(dir)
310
- iniNames = dirEntries.filter(n => n.endsWith('.ini'))
311
- } catch {
312
- iniNames = []
313
- }
314
- if (iniNames.length === 0) {
315
- fail(`${rel}: поруч немає жодного *.ini — додай values-*.ini для середовищ (див. nginx-default-tpl.mdc)`)
316
- } else {
317
- pass(`${rel}: поруч є *.ini (${iniNames.length})`)
318
- }
319
-
320
- for (const iniName of iniNames) {
321
- const iniPath = `${dir}/${iniName}`
322
- const iniRel = relative(root, iniPath) || iniPath
323
- let iniRaw
324
- try {
325
- iniRaw = await readFile(iniPath, 'utf8')
326
- } catch (error) {
327
- fail(`${iniRel}: не вдалося прочитати (${error instanceof Error ? error.message : String(error)})`)
328
- iniRaw = null
329
- }
330
- if (iniRaw !== null) {
331
- const keys = parseIniVariableNames(iniRaw)
332
- const miss = iniKeysMissingInTemplate(keys, content)
333
- if (miss) {
334
- fail(`${iniRel}: ${miss}`)
335
- }
336
- }
337
- }
402
+ await checkTemplateFile(abs, root, pass, fail)
338
403
  }
339
404
 
340
- const dockerPaths = await findDockerfilePaths(root)
341
- if (dockerPaths.length === 0) {
342
- fail(
343
- 'Є default.conf.template, але немає Dockerfile / Containerfile — додай gzip для статики та envsubst (див. nginx-default-tpl.mdc)'
344
- )
345
- } else {
346
- const bodies = await Promise.all(dockerPaths.map(p => readFile(p, 'utf8')))
347
- const gzipOk = bodies.some(body => dockerfileHasGzipStaticPipeline(body))
348
- const envOk = bodies.some(body => dockerfileHasEnvsSubstTemplate(body))
349
- if (gzipOk) {
350
- pass('Dockerfile: знайдено крок стиснення статики (find + gzip -k)')
351
- } else {
352
- fail('Dockerfile: потрібен RUN find … /usr/share/nginx/html … gzip -k (див. nginx-default-tpl.mdc)')
353
- }
354
- if (envOk) {
355
- pass('Dockerfile: знайдено envsubst для default.conf.template')
356
- } else {
357
- fail('Dockerfile: потрібен envsubst з default.conf.template (див. nginx-default-tpl.mdc)')
358
- }
359
- }
360
-
361
- if (existsSync('.vscode/extensions.json')) {
362
- const extRaw = await readFile('.vscode/extensions.json', 'utf8')
363
- const ext = JSON.parse(extRaw)
364
- if (ext.recommendations?.includes('ahmadalli.vscode-nginx-conf')) {
365
- pass('extensions.json містить ahmadalli.vscode-nginx-conf')
366
- } else {
367
- fail('extensions.json не містить ahmadalli.vscode-nginx-conf')
368
- }
369
- } else {
370
- fail('Очікується .vscode/extensions.json з ahmadalli.vscode-nginx-conf (див. nginx-default-tpl.mdc)')
371
- }
372
-
373
- if (existsSync('.vscode/settings.json')) {
374
- const settingsRaw = await readFile('.vscode/settings.json', 'utf8')
375
- const s = JSON.parse(settingsRaw)
376
- if (s['editor.formatOnSave'] === true) {
377
- pass('settings.json: editor.formatOnSave увімкнено')
378
- } else {
379
- fail('settings.json: увімкни editor.formatOnSave: true (див. nginx-default-tpl.mdc)')
380
- }
381
- if (s['[nginx]']?.['editor.defaultFormatter'] === 'ahmadalli.vscode-nginx-conf') {
382
- pass('settings.json: [nginx] defaultFormatter налаштовано')
383
- } else {
384
- fail('settings.json: [nginx].editor.defaultFormatter має бути ahmadalli.vscode-nginx-conf')
385
- }
386
- } else {
387
- fail('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
388
- }
405
+ await checkDockerfiles(root, pass, fail)
406
+ await checkVscodeNginx(pass, fail)
389
407
 
390
408
  return reporter.getExitCode()
391
409
  }
@@ -142,13 +142,155 @@ function npmTypesFileFromPackageField(typesField) {
142
142
  }
143
143
 
144
144
  /**
145
- * Перевіряє відповідність проєкту правилам npm-module.mdc
146
- * @returns {Promise<number>} 0 все OK, 1 — є проблеми
145
+ * Перевіряє поле types у npm/package.json.
146
+ * @param {unknown} typesField значення поля types
147
+ * @param {boolean} useSrcJsLayout чи використовується layout з npm/src
148
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
149
+ * @param {(msg: string) => void} failFn callback при помилці
147
150
  */
148
- export async function check() {
149
- const reporter = createCheckReporter()
150
- const { pass, fail } = reporter
151
+ function checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn) {
152
+ if (useSrcJsLayout) {
153
+ if (typesField === TYPES_INDEX) {
154
+ passFn(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
155
+ } else {
156
+ failFn(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
157
+ }
158
+ } else if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
159
+ passFn(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
160
+ } else {
161
+ failFn(
162
+ 'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
163
+ )
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Перевіряє npm/package.json на типи та files.
169
+ * @param {boolean} useSrcJsLayout чи використовується layout з npm/src
170
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
171
+ * @param {(msg: string) => void} failFn callback при помилці
172
+ */
173
+ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
174
+ if (!existsSync('npm/package.json')) return
175
+ const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
176
+ const typesField = npmPkg.types
177
+
178
+ checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn)
179
+
180
+ if (Array.isArray(npmPkg.files) && npmPkg.files.includes('types')) {
181
+ passFn('npm/package.json: files містить "types"')
182
+ } else {
183
+ failFn('npm/package.json: масив files має містити "types"')
184
+ }
185
+
186
+ const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
187
+ const missingTypesMsg = useSrcJsLayout
188
+ ? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
189
+ : `Файл для поля types не знайдено або шлях не під ./types/ — ${String(typesField)}`
190
+ if (typesPath && existsSync(typesPath)) {
191
+ passFn(`${typesPath} існує`)
192
+ } else {
193
+ failFn(missingTypesMsg)
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Перевіряє npm/tsconfig.emit-types.json.
199
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
200
+ * @param {(msg: string) => void} failFn callback при помилці
201
+ */
202
+ async function checkEmitTypesConfig(passFn, failFn) {
203
+ if (!existsSync(EMIT_TYPES_CONFIG)) {
204
+ failFn(
205
+ `Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
206
+ )
207
+ return
208
+ }
209
+ passFn(`${EMIT_TYPES_CONFIG} існує`)
210
+ let raw
211
+ try {
212
+ raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
213
+ } catch {
214
+ failFn(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
215
+ return
216
+ }
217
+ const issues = emitTypesConfigIssues(raw)
218
+ if (issues.length === 0) {
219
+ passFn(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
220
+ } else {
221
+ failFn(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Перевіряє npm-publish.yml workflow.
227
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
228
+ * @param {(msg: string) => void} failFn callback при помилці
229
+ */
230
+ async function checkPublishWorkflow(passFn, failFn) {
231
+ const publishWf = '.github/workflows/npm-publish.yml'
232
+ if (!existsSync(publishWf)) {
233
+ failFn(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
234
+ return
235
+ }
236
+ passFn(`${publishWf} існує`)
237
+ const pub = await readFile(publishWf, 'utf8')
238
+ const root = parseWorkflowYaml(pub)
239
+ if (root) {
240
+ const checks = [
241
+ {
242
+ ok: pushPathsIncludeNpmGlob(root),
243
+ pass: `${publishWf}: on.push.paths містить npm/**`,
244
+ fail: `${publishWf}: у on.push.paths має бути npm/**`
245
+ },
246
+ {
247
+ ok: pushHasMainBranch(root),
248
+ pass: `${publishWf}: очікується branch main`,
249
+ fail: `${publishWf}: очікується branch main`
250
+ },
251
+ {
252
+ ok: hasIdTokenWritePermission(root),
253
+ pass: `${publishWf}: permissions містить id-token: write (OIDC)`,
254
+ fail: `${publishWf}: permissions має містити id-token: write (OIDC)`
255
+ },
256
+ {
257
+ ok: hasNpmPublishStepWithPackage(root),
258
+ pass: `${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`,
259
+ fail: `${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`
260
+ }
261
+ ]
262
+ for (const c of checks) {
263
+ if (c.ok) {
264
+ passFn(c.pass)
265
+ } else {
266
+ failFn(c.fail)
267
+ }
268
+ }
269
+ return
270
+ }
271
+ const need = [
272
+ { sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
273
+ { sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
274
+ { sub: 'main', msg: `${publishWf}: очікується branch main` },
275
+ { sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
276
+ { sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
277
+ { sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
278
+ ]
279
+ for (const { sub, msg } of need) {
280
+ if (pub.includes(sub)) {
281
+ passFn(`${publishWf} містить «${sub}»`)
282
+ } else {
283
+ failFn(msg)
284
+ }
285
+ }
286
+ }
151
287
 
288
+ /**
289
+ * Перевіряє базову структуру монорепо (package.json, npm/, workspaces, npm/package.json).
290
+ * @param {(msg: string) => void} pass callback при успішній перевірці
291
+ * @param {(msg: string) => void} fail callback при помилці
292
+ */
293
+ async function checkNpmModuleBasicStructure(pass, fail) {
152
294
  if (existsSync('package.json')) {
153
295
  pass('package.json існує')
154
296
  } else {
@@ -168,8 +310,7 @@ export async function check() {
168
310
 
169
311
  if (existsSync('package.json')) {
170
312
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
171
- const ws = pkg.workspaces
172
- if (Array.isArray(ws) && ws.includes('npm')) {
313
+ if (Array.isArray(pkg.workspaces) && pkg.workspaces.includes('npm')) {
173
314
  pass('package.json workspaces містить "npm"')
174
315
  } else {
175
316
  fail('package.json workspaces має містити "npm"')
@@ -181,81 +322,33 @@ export async function check() {
181
322
  } else {
182
323
  fail('npm/package.json не існує — створи package.json для npm модуля')
183
324
  }
325
+ }
184
326
 
185
- const useSrcJsLayout = await npmSrcTreeHasJsFile()
186
-
187
- if (existsSync('npm/package.json')) {
188
- const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
189
- const typesField = npmPkg.types
327
+ /**
328
+ * Перевіряє відповідність проєкту правилам npm-module.mdc
329
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
330
+ */
331
+ export async function check() {
332
+ const reporter = createCheckReporter()
333
+ const { pass, fail } = reporter
190
334
 
191
- if (useSrcJsLayout) {
192
- if (typesField === TYPES_INDEX) {
193
- pass(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
194
- } else {
195
- fail(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
196
- }
197
- } else {
198
- if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
199
- pass(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
200
- } else {
201
- fail(
202
- 'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
203
- )
204
- }
205
- }
335
+ await checkNpmModuleBasicStructure(pass, fail)
206
336
 
207
- const files = npmPkg.files
208
- if (Array.isArray(files) && files.includes('types')) {
209
- pass('npm/package.json: files містить "types"')
210
- } else {
211
- fail('npm/package.json: масив files має містити "types"')
212
- }
337
+ const useSrcJsLayout = await npmSrcTreeHasJsFile()
213
338
 
214
- const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
215
- if (typesPath && existsSync(typesPath)) {
216
- pass(`${typesPath} існує`)
217
- } else {
218
- fail(
219
- useSrcJsLayout
220
- ? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
221
- : `Файл для поля types не знайдено або шлях не під ./types/ — ${String(typesField)}`
222
- )
223
- }
224
- }
339
+ await checkNpmPackageJson(useSrcJsLayout, pass, fail)
225
340
 
226
341
  if (!useSrcJsLayout) {
227
- if (existsSync(EMIT_TYPES_CONFIG)) {
228
- pass(`${EMIT_TYPES_CONFIG} існує`)
229
- let raw
230
- try {
231
- raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
232
- } catch {
233
- fail(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
234
- raw = null
235
- }
236
- if (raw) {
237
- const issues = emitTypesConfigIssues(raw)
238
- if (issues.length === 0) {
239
- pass(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
240
- } else {
241
- fail(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
242
- }
243
- }
244
- } else {
245
- fail(
246
- `Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
247
- )
248
- }
342
+ await checkEmitTypesConfig(pass, fail)
249
343
  }
250
344
 
345
+ const layoutLabel = useSrcJsLayout ? 'layout src' : 'tsconfig emit-types'
251
346
  const hk = await readHkConfig()
252
347
  if (hk) {
253
348
  pass(`${hk.path} існує`)
254
349
  const missing = useSrcJsLayout ? missingHkSrcLayoutFragments(hk.text) : missingHkEmitTypesConfigFragments(hk.text)
255
350
  if (missing.length === 0) {
256
- pass(
257
- `${hk.path}: pre-commit містить очікуваний виклик tsc (${useSrcJsLayout ? 'layout src' : 'tsconfig emit-types'})`
258
- )
351
+ pass(`${hk.path}: pre-commit містить очікуваний виклик tsc (${layoutLabel})`)
259
352
  } else {
260
353
  fail(`${hk.path}: онови pre-commit крок (npm-module.mdc); не знайдено: ${missing.join(', ')}`)
261
354
  }
@@ -269,53 +362,7 @@ export async function check() {
269
362
  fail('.github/workflows/ не існує')
270
363
  }
271
364
 
272
- const publishWf = '.github/workflows/npm-publish.yml'
273
- if (existsSync(publishWf)) {
274
- pass(`${publishWf} існує`)
275
- const pub = await readFile(publishWf, 'utf8')
276
- const root = parseWorkflowYaml(pub)
277
-
278
- if (root) {
279
- if (pushPathsIncludeNpmGlob(root)) {
280
- pass(`${publishWf}: on.push.paths містить npm/**`)
281
- } else {
282
- fail(`${publishWf}: у on.push.paths має бути npm/**`)
283
- }
284
- if (pushHasMainBranch(root)) {
285
- pass(`${publishWf}: очікується branch main`)
286
- } else {
287
- fail(`${publishWf}: очікується branch main`)
288
- }
289
- if (hasIdTokenWritePermission(root)) {
290
- pass(`${publishWf}: permissions містить id-token: write (OIDC)`)
291
- } else {
292
- fail(`${publishWf}: permissions має містити id-token: write (OIDC)`)
293
- }
294
- if (hasNpmPublishStepWithPackage(root)) {
295
- pass(`${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`)
296
- } else {
297
- fail(`${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`)
298
- }
299
- } else {
300
- const need = [
301
- { sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
302
- { sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
303
- { sub: 'main', msg: `${publishWf}: очікується branch main` },
304
- { sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
305
- { sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
306
- { sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
307
- ]
308
- for (const { sub, msg } of need) {
309
- if (pub.includes(sub)) {
310
- pass(`${publishWf} містить «${sub}»`)
311
- } else {
312
- fail(msg)
313
- }
314
- }
315
- }
316
- } else {
317
- fail(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
318
- }
365
+ await checkPublishWorkflow(pass, fail)
319
366
 
320
367
  return reporter.getExitCode()
321
368
  }