@nitra/cursor 1.8.105 → 1.8.108
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/bin/auto-rules.md +2 -2
- package/bin/n-cursor.js +5 -15
- package/mdc/js-pino.mdc +2 -2
- package/mdc/k8s.mdc +22 -12
- package/mdc/text.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +515 -528
- package/scripts/check-bun.mjs +106 -78
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-js-lint.mjs +256 -179
- package/scripts/check-js-pino.mjs +48 -3
- package/scripts/check-k8s.mjs +403 -34
- package/scripts/check-nginx-default-tpl.mjs +109 -91
- package/scripts/check-npm-module.mjs +163 -116
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +289 -209
- package/scripts/check-vue.mjs +108 -67
- package/scripts/utils/bunyan-imports.mjs +182 -0
- package/scripts/utils/gha-workflow.mjs +3 -1
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
* Перевіряє
|
|
146
|
-
* @
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|