@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
package/scripts/check-vue.mjs
CHANGED
|
@@ -52,99 +52,94 @@ function ukFilesCountPhrase(n) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Перевіряє залежності
|
|
56
|
-
* @param {string}
|
|
57
|
-
* @param {
|
|
58
|
-
* @param {
|
|
59
|
-
* @
|
|
55
|
+
* Перевіряє наявність залежності в об'єкті deps.
|
|
56
|
+
* @param {Record<string,string>} deps об'єкт залежностей
|
|
57
|
+
* @param {string} name ім'я пакета
|
|
58
|
+
* @param {string} prefix префікс повідомлення
|
|
59
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
60
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
61
|
+
* @param {string} hint підказка при відсутності
|
|
60
62
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const pkgPath = join(rootDir, 'package.json')
|
|
66
|
-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
67
|
-
const deps = pkg.dependencies || {}
|
|
68
|
-
const devDeps = pkg.devDependencies || {}
|
|
69
|
-
const allDeps = { ...deps, ...devDeps }
|
|
70
|
-
|
|
71
|
-
if (deps.vue) {
|
|
72
|
-
passFn(`${prefix}vue в dependencies: ${deps.vue}`)
|
|
63
|
+
function checkRequiredDep(deps, name, prefix, passFn, fail, hint = `${name} відсутній`) {
|
|
64
|
+
if (deps[name]) {
|
|
65
|
+
passFn(`${prefix}${name}: ${deps[name]}`)
|
|
73
66
|
} else {
|
|
74
|
-
fail(`${prefix}
|
|
67
|
+
fail(`${prefix}${hint}`)
|
|
75
68
|
}
|
|
69
|
+
}
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Перевіряє версію vite у devDependencies.
|
|
73
|
+
* @param {Record<string,string>} devDeps devDependencies з package.json
|
|
74
|
+
* @param {string} prefix параметр prefix
|
|
75
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
76
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
77
|
+
*/
|
|
78
|
+
function checkViteVersion(devDeps, prefix, passFn, fail) {
|
|
79
|
+
const v = devDeps.vite
|
|
80
|
+
if (!v) {
|
|
85
81
|
fail(`${prefix}vite відсутній в devDependencies`)
|
|
82
|
+
return
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
passFn(`${prefix}
|
|
90
|
-
} else {
|
|
91
|
-
fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (allDeps['vue-macros']) {
|
|
95
|
-
passFn(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
|
|
96
|
-
} else {
|
|
97
|
-
fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (allDeps['unplugin-auto-import']) {
|
|
101
|
-
passFn(`${prefix}unplugin-auto-import присутній`)
|
|
102
|
-
} else {
|
|
103
|
-
fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (allDeps['vite-plugin-vue-layouts-next']) {
|
|
107
|
-
passFn(`${prefix}vite-plugin-vue-layouts-next присутній`)
|
|
84
|
+
const match = v.match(MAJOR_VERSION_RE)
|
|
85
|
+
if (match && Number(match[1]) >= 8) {
|
|
86
|
+
passFn(`${prefix}vite >= 8: ${v}`)
|
|
108
87
|
} else {
|
|
109
|
-
fail(`${prefix}vite
|
|
88
|
+
fail(`${prefix}vite має бути >= 8, знайдено: ${v}`)
|
|
110
89
|
}
|
|
90
|
+
}
|
|
111
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Перевіряє vite.config на наявність VueMacros і AutoImport.
|
|
94
|
+
* @param {string} rootDir параметр rootDir
|
|
95
|
+
* @param {string} prefix параметр prefix
|
|
96
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
97
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
98
|
+
*/
|
|
99
|
+
async function checkViteConfig(rootDir, prefix, passFn, fail) {
|
|
112
100
|
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
113
101
|
const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
|
|
114
|
-
if (viteConfig) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
if (!viteConfig) {
|
|
103
|
+
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const content = await readFile(join(rootDir, viteConfig), 'utf8')
|
|
107
|
+
const checks = [
|
|
108
|
+
{ token: 'VueMacros', ok: `${viteConfig} використовує VueMacros`, err: `${viteConfig} не містить VueMacros` },
|
|
109
|
+
{ token: 'AutoImport', ok: `${viteConfig} використовує AutoImport`, err: `${viteConfig} не містить AutoImport` }
|
|
110
|
+
]
|
|
111
|
+
for (const { token, ok, err } of checks) {
|
|
112
|
+
if (content.includes(token)) {
|
|
113
|
+
passFn(`${prefix}${ok}`)
|
|
124
114
|
} else {
|
|
125
|
-
fail(`${prefix}${
|
|
115
|
+
fail(`${prefix}${err}`)
|
|
126
116
|
}
|
|
127
|
-
} else {
|
|
128
|
-
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
129
117
|
}
|
|
118
|
+
}
|
|
130
119
|
|
|
131
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Сканує джерела пакета на заборонені value-імпорти з vue.
|
|
122
|
+
* @param {string} rootDir параметр rootDir
|
|
123
|
+
* @param {string} absPackageRoot параметр absPackageRoot
|
|
124
|
+
* @param {string} prefix параметр prefix
|
|
125
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
126
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
127
|
+
*/
|
|
128
|
+
async function checkVueImportViolations(rootDir, absPackageRoot, prefix, passFn, fail) {
|
|
132
129
|
/** @type {string[]} */
|
|
133
130
|
const sourcePaths = []
|
|
134
131
|
await walkDir(absPackageRoot, absPath => {
|
|
135
132
|
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
136
|
-
if (shouldSkipFileForVueImportScan(rel)
|
|
137
|
-
|
|
133
|
+
if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
|
|
134
|
+
sourcePaths.push(absPath)
|
|
138
135
|
}
|
|
139
|
-
sourcePaths.push(absPath)
|
|
140
136
|
})
|
|
141
137
|
|
|
142
138
|
let importViolations = 0
|
|
143
139
|
for (const absPath of sourcePaths) {
|
|
144
140
|
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
145
141
|
const content = await readFile(absPath, 'utf8')
|
|
146
|
-
const
|
|
147
|
-
for (const v of hits) {
|
|
142
|
+
for (const v of findForbiddenVueImportsInSourceFile(content, rel)) {
|
|
148
143
|
importViolations++
|
|
149
144
|
fail(`${prefix}${rel}:${v.line} — прибери явний value-імпорт з 'vue' (unplugin-auto-import): ${v.snippet}`)
|
|
150
145
|
}
|
|
@@ -156,6 +151,52 @@ async function checkVuePackage(rootDir, fail, passFn) {
|
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Перевіряє залежності та vite.config одного Vue-пакета.
|
|
156
|
+
* @param {string} rootDir відносний шлях до пакета
|
|
157
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
158
|
+
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
159
|
+
* @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
|
|
160
|
+
*/
|
|
161
|
+
async function checkVuePackage(rootDir, fail, passFn) {
|
|
162
|
+
const prefix = `[${packageLabel(rootDir)}] `
|
|
163
|
+
const pkg = JSON.parse(await readFile(join(rootDir, 'package.json'), 'utf8'))
|
|
164
|
+
const deps = pkg.dependencies || {}
|
|
165
|
+
const devDeps = pkg.devDependencies || {}
|
|
166
|
+
const allDeps = { ...deps, ...devDeps }
|
|
167
|
+
|
|
168
|
+
checkRequiredDep(deps, 'vue', prefix, passFn, fail, 'vue відсутній в dependencies')
|
|
169
|
+
checkViteVersion(devDeps, prefix, passFn, fail)
|
|
170
|
+
checkRequiredDep(
|
|
171
|
+
devDeps,
|
|
172
|
+
'@vitejs/plugin-vue',
|
|
173
|
+
prefix,
|
|
174
|
+
passFn,
|
|
175
|
+
fail,
|
|
176
|
+
'@vitejs/plugin-vue відсутній в devDependencies'
|
|
177
|
+
)
|
|
178
|
+
checkRequiredDep(allDeps, 'vue-macros', prefix, passFn, fail, 'vue-macros відсутній — bun add -d vue-macros')
|
|
179
|
+
checkRequiredDep(
|
|
180
|
+
allDeps,
|
|
181
|
+
'unplugin-auto-import',
|
|
182
|
+
prefix,
|
|
183
|
+
passFn,
|
|
184
|
+
fail,
|
|
185
|
+
'unplugin-auto-import відсутній — bun add -d unplugin-auto-import'
|
|
186
|
+
)
|
|
187
|
+
checkRequiredDep(
|
|
188
|
+
allDeps,
|
|
189
|
+
'vite-plugin-vue-layouts-next',
|
|
190
|
+
prefix,
|
|
191
|
+
passFn,
|
|
192
|
+
fail,
|
|
193
|
+
'vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
await checkViteConfig(rootDir, prefix, passFn, fail)
|
|
197
|
+
await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
|
|
198
|
+
}
|
|
199
|
+
|
|
159
200
|
/**
|
|
160
201
|
* Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
|
|
161
202
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить імпорти з `@nitra/bunyan` (і застарілого `bunyan`) у джерелах — їх треба замінити
|
|
3
|
+
* на `@nitra/pino` згідно з js-pino.mdc.
|
|
4
|
+
*
|
|
5
|
+
* Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
|
|
6
|
+
* Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
|
|
7
|
+
* щоб правило працювало й у CommonJS/інлайн-завантаженні.
|
|
8
|
+
*
|
|
9
|
+
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
|
|
10
|
+
* результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
|
|
11
|
+
*/
|
|
12
|
+
import { parseSync } from 'oxc-parser'
|
|
13
|
+
|
|
14
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
15
|
+
const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Мова для Oxc за шляхом файлу (розширення).
|
|
19
|
+
* @param {string} filePath віртуальний або реальний шлях до файлу
|
|
20
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
|
|
21
|
+
*/
|
|
22
|
+
function langFromPath(filePath) {
|
|
23
|
+
const lower = filePath.toLowerCase()
|
|
24
|
+
if (lower.endsWith('.tsx')) {
|
|
25
|
+
return 'tsx'
|
|
26
|
+
}
|
|
27
|
+
if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
|
|
28
|
+
return 'ts'
|
|
29
|
+
}
|
|
30
|
+
if (lower.endsWith('.jsx')) {
|
|
31
|
+
return 'jsx'
|
|
32
|
+
}
|
|
33
|
+
return 'js'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Номер рядка (1-based) за зміщенням у буфері.
|
|
38
|
+
* @param {string} content повний текст файлу
|
|
39
|
+
* @param {number} offset байтове зміщення початку фрагмента
|
|
40
|
+
* @returns {number} номер рядка від 1
|
|
41
|
+
*/
|
|
42
|
+
function offsetToLine(content, offset) {
|
|
43
|
+
let line = 1
|
|
44
|
+
const n = Math.min(offset, content.length)
|
|
45
|
+
for (let i = 0; i < n; i++) {
|
|
46
|
+
if (content.codePointAt(i) === 10) {
|
|
47
|
+
line++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return line
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Стискає пробіли для повідомлення про порушення.
|
|
55
|
+
* @param {string} s фрагмент коду
|
|
56
|
+
* @returns {string} скорочений однорядковий рядок
|
|
57
|
+
*/
|
|
58
|
+
function normalizeSnippet(s) {
|
|
59
|
+
return s.replaceAll(/\s+/g, ' ').trim().slice(0, 160)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
|
|
64
|
+
* @param {any} node вузол AST
|
|
65
|
+
* @returns {string | null} ім'я модуля з аргументу, інакше `null`
|
|
66
|
+
*/
|
|
67
|
+
function requireCallModule(node) {
|
|
68
|
+
if (!node || node.type !== 'CallExpression') return null
|
|
69
|
+
const callee = node.callee
|
|
70
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return null
|
|
71
|
+
const arg = node.arguments?.[0]
|
|
72
|
+
if (!arg || arg.type !== 'Literal' || typeof arg.value !== 'string') return null
|
|
73
|
+
return arg.value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
|
|
78
|
+
* @param {any} node вузол AST
|
|
79
|
+
* @returns {string | null} ім'я модуля, інакше `null`
|
|
80
|
+
*/
|
|
81
|
+
function dynamicImportModule(node) {
|
|
82
|
+
if (!node || node.type !== 'ImportExpression') return null
|
|
83
|
+
const src = node.source
|
|
84
|
+
if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return null
|
|
85
|
+
return src.value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Простий рекурсивний обхід AST: заходимо в усі об'єкти/масиви, щоб знайти require/import-вузли.
|
|
90
|
+
* @param {any} node корінь або під-вузол AST
|
|
91
|
+
* @param {(n: any) => void} visit виклик для кожного об'єкта-вузла
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
function walkAst(node, visit) {
|
|
95
|
+
if (!node || typeof node !== 'object') return
|
|
96
|
+
if (Array.isArray(node)) {
|
|
97
|
+
for (const item of node) walkAst(item, visit)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (typeof node.type === 'string') {
|
|
101
|
+
visit(node)
|
|
102
|
+
}
|
|
103
|
+
for (const key of Object.keys(node)) {
|
|
104
|
+
if (key === 'parent') continue
|
|
105
|
+
const v = node[key]
|
|
106
|
+
if (v && typeof v === 'object') walkAst(v, visit)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Знаходить заборонені імпорти/require з `@nitra/bunyan` у тексті.
|
|
112
|
+
* @param {string} content вихідний код
|
|
113
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
114
|
+
* @returns {{ line: number, snippet: string, module: string }[]} список порушень
|
|
115
|
+
*/
|
|
116
|
+
export function findBunyanImportsInText(content, virtualPath = 'scan.ts') {
|
|
117
|
+
const pathForLang = virtualPath || 'scan.ts'
|
|
118
|
+
const lang = langFromPath(pathForLang)
|
|
119
|
+
let result
|
|
120
|
+
try {
|
|
121
|
+
result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
|
|
122
|
+
} catch {
|
|
123
|
+
return []
|
|
124
|
+
}
|
|
125
|
+
if (result.errors?.length) {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @type {{ line: number, snippet: string, module: string }[]} */
|
|
130
|
+
const out = []
|
|
131
|
+
|
|
132
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
133
|
+
const mod = imp.moduleRequest?.value
|
|
134
|
+
if (mod && FORBIDDEN_MODULES.has(mod)) {
|
|
135
|
+
out.push({
|
|
136
|
+
line: offsetToLine(content, imp.start),
|
|
137
|
+
snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
|
|
138
|
+
module: mod
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
walkAst(result.program, node => {
|
|
144
|
+
const reqMod = requireCallModule(node)
|
|
145
|
+
if (reqMod && FORBIDDEN_MODULES.has(reqMod)) {
|
|
146
|
+
out.push({
|
|
147
|
+
line: offsetToLine(content, node.start),
|
|
148
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
149
|
+
module: reqMod
|
|
150
|
+
})
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const dynMod = dynamicImportModule(node)
|
|
154
|
+
if (dynMod && FORBIDDEN_MODULES.has(dynMod)) {
|
|
155
|
+
out.push({
|
|
156
|
+
line: offsetToLine(content, node.start),
|
|
157
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
158
|
+
module: dynMod
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я).
|
|
168
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
169
|
+
* @returns {boolean} `true`, якщо розширення підходить для пошуку імпорту
|
|
170
|
+
*/
|
|
171
|
+
export function isBunyanScanSourceFile(relativePath) {
|
|
172
|
+
return SOURCE_FILE_RE.test(relativePath)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Чи слід пропустити файл під час обходу пакета (декларації типів).
|
|
177
|
+
* @param {string} relativePosix шлях з posix-слешами
|
|
178
|
+
* @returns {boolean} `true`, якщо файл не сканувати
|
|
179
|
+
*/
|
|
180
|
+
export function shouldSkipFileForBunyanScan(relativePosix) {
|
|
181
|
+
return relativePosix.endsWith('.d.ts')
|
|
182
|
+
}
|
|
@@ -324,7 +324,9 @@ function workflowJobSteps(job) {
|
|
|
324
324
|
if (!Array.isArray(steps)) {
|
|
325
325
|
return []
|
|
326
326
|
}
|
|
327
|
-
return steps.flatMap(step =>
|
|
327
|
+
return steps.flatMap(step =>
|
|
328
|
+
step && typeof step === 'object' ? [/** @type {Record<string, unknown>} */ (step)] : []
|
|
329
|
+
)
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
/**
|