@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.
@@ -52,99 +52,94 @@ function ukFilesCountPhrase(n) {
52
52
  }
53
53
 
54
54
  /**
55
- * Перевіряє залежності та vite.config одного Vue-пакета.
56
- * @param {string} rootDir відносний шлях до пакета
57
- * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
58
- * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
59
- * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
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
- async function checkVuePackage(rootDir, fail, passFn) {
62
- const label = packageLabel(rootDir)
63
- const prefix = `[${label}] `
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}vue відсутній в dependencies`)
67
+ fail(`${prefix}${hint}`)
75
68
  }
69
+ }
76
70
 
77
- if (devDeps.vite) {
78
- const match = devDeps.vite.match(MAJOR_VERSION_RE)
79
- if (match && Number(match[1]) >= 8) {
80
- passFn(`${prefix}vite >= 8: ${devDeps.vite}`)
81
- } else {
82
- fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
83
- }
84
- } else {
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 (devDeps['@vitejs/plugin-vue']) {
89
- passFn(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
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-plugin-vue-layouts-next відсутній bun add -d vite-plugin-vue-layouts-next`)
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
- const relConfig = join(rootDir, viteConfig)
116
- const content = await readFile(relConfig, 'utf8')
117
- if (content.includes('VueMacros')) {
118
- passFn(`${prefix}${viteConfig} використовує VueMacros`)
119
- } else {
120
- fail(`${prefix}${viteConfig} не містить VueMacros`)
121
- }
122
- if (content.includes('AutoImport')) {
123
- passFn(`${prefix}${viteConfig} використовує AutoImport`)
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}${viteConfig} не містить AutoImport`)
115
+ fail(`${prefix}${err}`)
126
116
  }
127
- } else {
128
- fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
129
117
  }
118
+ }
130
119
 
131
- const absPackageRoot = join(process.cwd(), rootDir)
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) || !isVueImportScanSourceFile(rel)) {
137
- return
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 hits = findForbiddenVueImportsInSourceFile(content, rel)
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 => (step && typeof step === 'object' ? [/** @type {Record<string, unknown>} */ (step)] : []))
327
+ return steps.flatMap(step =>
328
+ step && typeof step === 'object' ? [/** @type {Record<string, unknown>} */ (step)] : []
329
+ )
328
330
  }
329
331
 
330
332
  /**