@nitra/cursor 1.8.24 → 1.8.29

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/mdc/vue.mdc CHANGED
@@ -217,10 +217,10 @@ export default defineConfig({
217
217
 
218
218
  Потрібно використовувати Vite версії 8 та вище для frontend проекту на Vue.
219
219
 
220
- Потрібно використовувати unplugin-auto-import для автоматичного імпортування компонентів, composables, utils та інших функцій і прибирати з Vue sfc імпорти які їх дублюють.
220
+ Потрібно використовувати unplugin-auto-import для автоматичного імпортування компонентів, composables, utils та інших функцій і прибирати з файлів усередині Vite проектів відповідні ручні імпорти, зокрема рядки виду `import { … } from 'vue'` — API Vue (`ref`, `computed`, `watch` тощо) мають підставлятися через auto-import, а не дублюватися явним імпортом з модуля `vue`.
221
221
 
222
222
  Потрібно використовувати vite-plugin-vue-layouts-next для автоматичного імпортування layout компонентів.
223
223
 
224
224
  ## Перевірка
225
225
 
226
- `npx @nitra/cursor check vue`
226
+ `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, а також обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue`; дозволені лише type-only та side-effect `import 'vue'`. Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.24",
3
+ "version": "1.8.29",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -36,6 +36,7 @@
36
36
  "test": "bun test tests"
37
37
  },
38
38
  "dependencies": {
39
+ "oxc-parser": "^0.124.0",
39
40
  "yaml": "^2.8.3"
40
41
  },
41
42
  "engines": {
@@ -7,13 +7,22 @@
7
7
  * перед `uses: ./…/setup-bun-deps` у workflow — `actions/checkout` (runner інакше не бачить локальний action).
8
8
  *
9
9
  * Заборонено дублювати кроки встановлення Bun та кешування безпосередньо у workflow файлах
10
- * (oven-sh/setup-bun, actions/cache, bun install).
10
+ * (oven-sh/setup-bun, actions/cache, bun install). Перевірки `uses`/`run` виконуються після **YAML parse**
11
+ * (`yaml`), щоб не спрацьовувати на випадкові збіги в коментарях або поза кроками.
11
12
  */
12
13
  import { existsSync } from 'node:fs'
13
14
  import { readdir, readFile } from 'node:fs/promises'
14
15
  import { join } from 'node:path'
15
16
 
16
17
  import { pass } from './utils/pass.mjs'
18
+ import {
19
+ anyRunStepIncludes,
20
+ eventPathsIncludeExact,
21
+ findForbiddenUsesOrRunPatterns,
22
+ hasAnyStepUsesContaining,
23
+ hasCheckoutBeforeLocalSetupBunDeps,
24
+ parseWorkflowYaml
25
+ } from './utils/gha-workflow.mjs'
17
26
 
18
27
  /** Шаблони наявності MegaLinter у вмісті workflow */
19
28
  const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/megalinter/i]
@@ -21,8 +30,19 @@ const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/
21
30
  /** Типові конфіги MegaLinter у корені репо */
22
31
  const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-linter.yaml']
23
32
 
33
+ /** Локальні composite setup-bun-deps (ga.mdc). */
34
+ const SETUP_BUN_PATTERNS = ['./.github/actions/setup-bun-deps', './npm/github-actions/setup-bun-deps']
35
+
36
+ /** Заборонені підрядки лише в кроках uses/run. */
37
+ const FORBIDDEN_BUN_PATTERNS = [
38
+ { pattern: 'oven-sh/setup-bun', msg: 'використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun' },
39
+ { pattern: 'actions/cache', msg: 'використовуй .github/actions/setup-bun-deps замість actions/cache' },
40
+ { pattern: 'bun install', msg: 'використовуй .github/actions/setup-bun-deps замість bun install' }
41
+ ]
42
+
24
43
  /**
25
44
  * Якщо workflow викликає локальний setup-bun-deps, раніше у файлі має бути `actions/checkout@v…` (ga.mdc).
45
+ * Fallback: сирий текст, якщо YAML не вдається розібрати.
26
46
  * @param {string} relPath шлях для повідомлень
27
47
  * @param {string} content вміст YAML
28
48
  * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
@@ -30,9 +50,22 @@ const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-
30
50
  * @returns {void}
31
51
  */
32
52
  function verifyCheckoutBeforeLocalSetupBunDeps(relPath, content, failFn, passFn) {
33
- const patterns = ['./.github/actions/setup-bun-deps', './npm/github-actions/setup-bun-deps']
53
+ const root = parseWorkflowYaml(content)
54
+ if (root) {
55
+ if (!hasAnyStepUsesContaining(root, SETUP_BUN_PATTERNS)) {
56
+ return
57
+ }
58
+ if (!hasCheckoutBeforeLocalSetupBunDeps(root, SETUP_BUN_PATTERNS)) {
59
+ failFn(
60
+ `${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
61
+ )
62
+ return
63
+ }
64
+ passFn(`${relPath}: перед setup-bun-deps є checkout`)
65
+ return
66
+ }
34
67
  let idxSetup = -1
35
- for (const p of patterns) {
68
+ for (const p of SETUP_BUN_PATTERNS) {
36
69
  const i = content.indexOf(p)
37
70
  if (i !== -1 && (idxSetup === -1 || i < idxSetup)) {
38
71
  idxSetup = i
@@ -52,27 +85,33 @@ function verifyCheckoutBeforeLocalSetupBunDeps(relPath, content, failFn, passFn)
52
85
  }
53
86
 
54
87
  /**
55
- * Перевіряє, чи не використовуються oven-sh/setup-bun або actions/cache безпосередньо у workflow.
88
+ * Перевіряє заборонені кроки Bun/cache/install у `uses` та `run`.
56
89
  * @param {string} relPath шлях для повідомлень
57
90
  * @param {string} content вміст YAML
58
91
  * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
59
92
  * @param {(msg: string) => void} passFn реєструє успішну перевірку
93
+ * @returns {void}
60
94
  */
61
95
  function verifyNoDirectBunOrCache(relPath, content, failFn, passFn) {
62
- const forbidden = [
63
- { pattern: 'oven-sh/setup-bun', msg: 'використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun' },
64
- { pattern: 'actions/cache', msg: 'використовуй .github/actions/setup-bun-deps замість actions/cache' },
65
- { pattern: 'bun install', msg: 'використовуй .github/actions/setup-bun-deps замість bun install' }
66
- ]
67
-
96
+ const root = parseWorkflowYaml(content)
97
+ if (root) {
98
+ const hits = findForbiddenUsesOrRunPatterns(root, FORBIDDEN_BUN_PATTERNS)
99
+ if (hits.length === 0) {
100
+ passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
101
+ } else {
102
+ for (const h of hits) {
103
+ failFn(`${relPath}: ${h.msg} (ga.mdc)`)
104
+ }
105
+ }
106
+ return
107
+ }
68
108
  let foundForbidden = false
69
- for (const { pattern, msg } of forbidden) {
109
+ for (const { pattern, msg } of FORBIDDEN_BUN_PATTERNS) {
70
110
  if (content.includes(pattern)) {
71
111
  failFn(`${relPath}: ${msg} (ga.mdc)`)
72
112
  foundForbidden = true
73
113
  }
74
114
  }
75
-
76
115
  if (!foundForbidden) {
77
116
  passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
78
117
  }
@@ -109,7 +148,9 @@ export async function check() {
109
148
 
110
149
  const yamlFiles = files.filter(f => f.endsWith('.yaml'))
111
150
  if (yamlFiles.length > 0) {
112
- for (const f of yamlFiles) fail(`Workflow з розширенням .yaml: ${wfDir}/${f} — перейменуй на .yml`)
151
+ for (const f of yamlFiles) {
152
+ fail(`Workflow з розширенням .yaml: ${wfDir}/${f} — перейменуй на .yml`)
153
+ }
113
154
  } else {
114
155
  pass('Всі workflows мають розширення .yml')
115
156
  }
@@ -124,7 +165,10 @@ export async function check() {
124
165
 
125
166
  if (files.includes('apply-k8s.yml')) {
126
167
  const content = await readFile(`${wfDir}/apply-k8s.yml`, 'utf8')
127
- if (content.includes('**/k8s/**/*.yaml')) {
168
+ const root = parseWorkflowYaml(content)
169
+ const ok =
170
+ root && eventPathsIncludeExact(root, 'push', '**/k8s/**/*.yaml') ? true : content.includes('**/k8s/**/*.yaml')
171
+ if (ok) {
128
172
  pass('apply-k8s.yml має правильний paths trigger')
129
173
  } else {
130
174
  fail('apply-k8s.yml не містить paths: **/k8s/**/*.yaml')
@@ -133,7 +177,10 @@ export async function check() {
133
177
 
134
178
  if (files.includes('apply-nats-consumer.yml')) {
135
179
  const content = await readFile(`${wfDir}/apply-nats-consumer.yml`, 'utf8')
136
- if (content.includes('**/consumer.yaml')) {
180
+ const root = parseWorkflowYaml(content)
181
+ const ok =
182
+ root && eventPathsIncludeExact(root, 'push', '**/consumer.yaml') ? true : content.includes('**/consumer.yaml')
183
+ if (ok) {
137
184
  pass('apply-nats-consumer.yml має правильний paths trigger')
138
185
  } else {
139
186
  fail('apply-nats-consumer.yml не містить paths: **/consumer.yaml')
@@ -216,15 +263,30 @@ export async function check() {
216
263
  const lintGaWf = join(wfDir, 'lint-ga.yml')
217
264
  if (existsSync(lintGaWf)) {
218
265
  const lgContent = await readFile(lintGaWf, 'utf8')
219
- if (lgContent.includes('bun run lint-ga')) {
220
- pass('lint-ga.yml викликає bun run lint-ga')
221
- } else {
222
- fail('lint-ga.yml: крок має містити bun run lint-ga')
223
- }
224
- if (lgContent.includes('astral-sh/setup-uv')) {
225
- pass('lint-ga.yml містить astral-sh/setup-uv')
266
+ const root = parseWorkflowYaml(lgContent)
267
+ if (root) {
268
+ if (anyRunStepIncludes(root, 'bun run lint-ga')) {
269
+ pass('lint-ga.yml викликає bun run lint-ga')
270
+ } else {
271
+ fail('lint-ga.yml: крок має містити bun run lint-ga')
272
+ }
273
+ const usesFlat = hasAnyStepUsesContaining(root, ['astral-sh/setup-uv'])
274
+ if (usesFlat || lgContent.includes('astral-sh/setup-uv')) {
275
+ pass('lint-ga.yml містить astral-sh/setup-uv')
276
+ } else {
277
+ fail('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
278
+ }
226
279
  } else {
227
- fail('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
280
+ if (lgContent.includes('bun run lint-ga')) {
281
+ pass('lint-ga.yml викликає bun run lint-ga')
282
+ } else {
283
+ fail('lint-ga.yml: крок має містити bun run lint-ga')
284
+ }
285
+ if (lgContent.includes('astral-sh/setup-uv')) {
286
+ pass('lint-ga.yml містить astral-sh/setup-uv')
287
+ } else {
288
+ fail('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
289
+ }
228
290
  }
229
291
  }
230
292
 
@@ -10,6 +10,7 @@ import { existsSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
 
12
12
  import { pass } from './utils/pass.mjs'
13
+ import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
13
14
 
14
15
  /** Очікуваний локальний скрипт (oxlint без bunx; eslint/jscpd через bunx). */
15
16
  export const CANONICAL_LINT_JS = 'oxlint --fix && bunx eslint --fix . && bunx jscpd .'
@@ -154,26 +155,38 @@ export async function check() {
154
155
  if (existsSync('.github/workflows/lint-js.yml')) {
155
156
  const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
156
157
  pass('lint-js.yml існує')
157
- const checks = [
158
- ['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
159
- ['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
160
- ['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
161
- ['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
162
- ['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
163
- ['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
164
- ]
165
- for (const [needle, errMsg] of checks) {
166
- if (content.includes(needle)) {
167
- pass(`lint-js.yml містить: ${needle}`)
158
+ const root = parseWorkflowYaml(content)
159
+ if (root) {
160
+ const v = verifyLintJsWorkflowStructure(root)
161
+ if (v.ok) {
162
+ pass('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
168
163
  } else {
169
- fail(errMsg)
164
+ for (const msg of v.failures) {
165
+ fail(`lint-js.yml: ${msg}`)
166
+ }
167
+ }
168
+ } else {
169
+ const checks = [
170
+ ['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
171
+ ['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
172
+ ['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
173
+ ['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
174
+ ['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
175
+ ['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
176
+ ]
177
+ for (const [needle, errMsg] of checks) {
178
+ if (content.includes(needle)) {
179
+ pass(`lint-js.yml містить: ${needle}`)
180
+ } else {
181
+ fail(errMsg)
182
+ }
183
+ }
184
+ if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
185
+ fail('lint-js.yml: у CI не використовуй oxlint --fix (лише bunx oxlint)')
186
+ }
187
+ if (content.includes('eslint --fix')) {
188
+ fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
170
189
  }
171
- }
172
- if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
173
- fail('lint-js.yml: у CI не використовуй oxlint --fix (лише bunx oxlint)')
174
- }
175
- if (content.includes('eslint --fix')) {
176
- fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
177
190
  }
178
191
  } else {
179
192
  fail('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
@@ -2,11 +2,19 @@
2
2
  * Перевіряє структуру npm-модуля в монорепо за правилом npm-module.mdc.
3
3
  *
4
4
  * Workspace `npm/`, `npm/package.json`, workflow `npm-publish.yml` з OIDC, `on.push.paths` з glob для каталогу npm (див. npm-module.mdc).
5
+ * Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
5
6
  */
6
7
  import { existsSync } from 'node:fs'
7
8
  import { readFile, stat } from 'node:fs/promises'
8
9
 
9
10
  import { pass } from './utils/pass.mjs'
11
+ import {
12
+ hasIdTokenWritePermission,
13
+ hasNpmPublishStepWithPackage,
14
+ parseWorkflowYaml,
15
+ pushHasMainBranch,
16
+ pushPathsIncludeNpmGlob
17
+ } from './utils/gha-workflow.mjs'
10
18
 
11
19
  /**
12
20
  * Перевіряє відповідність проєкту правилам npm-module.mdc
@@ -62,19 +70,44 @@ export async function check() {
62
70
  if (existsSync(publishWf)) {
63
71
  pass(`${publishWf} існує`)
64
72
  const pub = await readFile(publishWf, 'utf8')
65
- const need = [
66
- { sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
67
- { sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
68
- { sub: 'main', msg: `${publishWf}: очікується branch main` },
69
- { sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
70
- { sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
71
- { sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
72
- ]
73
- for (const { sub, msg } of need) {
74
- if (pub.includes(sub)) {
75
- pass(`${publishWf} містить «${sub}»`)
73
+ const root = parseWorkflowYaml(pub)
74
+
75
+ if (root) {
76
+ if (pushPathsIncludeNpmGlob(root)) {
77
+ pass(`${publishWf}: on.push.paths містить npm/**`)
78
+ } else {
79
+ fail(`${publishWf}: у on.push.paths має бути npm/**`)
80
+ }
81
+ if (pushHasMainBranch(root)) {
82
+ pass(`${publishWf}: очікується branch main`)
83
+ } else {
84
+ fail(`${publishWf}: очікується branch main`)
85
+ }
86
+ if (hasIdTokenWritePermission(root)) {
87
+ pass(`${publishWf}: permissions містить id-token: write (OIDC)`)
76
88
  } else {
77
- fail(msg)
89
+ fail(`${publishWf}: permissions має містити id-token: write (OIDC)`)
90
+ }
91
+ if (hasNpmPublishStepWithPackage(root)) {
92
+ pass(`${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`)
93
+ } else {
94
+ fail(`${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`)
95
+ }
96
+ } else {
97
+ const need = [
98
+ { sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
99
+ { sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
100
+ { sub: 'main', msg: `${publishWf}: очікується branch main` },
101
+ { sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
102
+ { sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
103
+ { sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
104
+ ]
105
+ for (const { sub, msg } of need) {
106
+ if (pub.includes(sub)) {
107
+ pass(`${publishWf} містить «${sub}»`)
108
+ } else {
109
+ fail(msg)
110
+ }
78
111
  }
79
112
  }
80
113
  } else {
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs'
8
8
  import { readFile } from 'node:fs/promises'
9
9
 
10
10
  import { pass } from './utils/pass.mjs'
11
+ import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-workflow.mjs'
11
12
 
12
13
  /**
13
14
  * Перевіряє відповідність проєкту правилам style-lint.mdc
@@ -54,8 +55,10 @@ export async function check() {
54
55
  if (existsSync('.github/workflows/lint-style.yml')) {
55
56
  const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
56
57
  pass('lint-style.yml існує')
57
- if (content.includes('stylelint')) {
58
- pass('lint-style.yml містить stylelint')
58
+ const root = parseWorkflowYaml(content)
59
+ const ok = root ? anyRunStepIncludesStylelint(root) : content.includes('stylelint')
60
+ if (ok) {
61
+ pass('lint-style.yml містить stylelint у кроці run')
59
62
  } else {
60
63
  fail('lint-style.yml не містить виклик stylelint')
61
64
  }
@@ -13,6 +13,7 @@ import { existsSync } from 'node:fs'
13
13
  import { readFile } from 'node:fs/promises'
14
14
 
15
15
  import { pass } from './utils/pass.mjs'
16
+ import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
16
17
 
17
18
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
18
19
  const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
@@ -194,7 +195,9 @@ export async function check() {
194
195
 
195
196
  if (existsSync('.github/workflows/lint-text.yml')) {
196
197
  const wf = await readFile('.github/workflows/lint-text.yml', 'utf8')
197
- if (wf.includes('bun run lint-text')) {
198
+ const root = parseWorkflowYaml(wf)
199
+ const ok = root ? anyRunStepIncludes(root, 'bun run lint-text') : wf.includes('bun run lint-text')
200
+ if (ok) {
198
201
  pass('lint-text.yml викликає bun run lint-text')
199
202
  } else {
200
203
  fail('lint-text.yml має містити крок bun run lint-text')
@@ -3,12 +3,21 @@
3
3
  *
4
4
  * Версії Vite та плагінів, vue-macros, auto-import, layouts, вміст `vite.config`;
5
5
  * у репозиторії — рекомендацію розширення Vue.volar.
6
+ *
7
+ * Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
8
+ * через **oxc-parser** (`module.staticImports`; див. `utils/vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
6
9
  */
7
10
  import { existsSync } from 'node:fs'
8
11
  import { readFile } from 'node:fs/promises'
9
- import { join } from 'node:path'
12
+ import { join, relative } from 'node:path'
10
13
 
11
14
  import { pass } from './utils/pass.mjs'
15
+ import {
16
+ findForbiddenVueImportsInSourceFile,
17
+ isVueImportScanSourceFile,
18
+ shouldSkipFileForVueImportScan
19
+ } from './utils/vue-forbidden-imports.mjs'
20
+ import { walkDir } from './utils/walkDir.mjs'
12
21
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
13
22
 
14
23
  /**
@@ -20,11 +29,31 @@ function packageLabel(rootDir) {
20
29
  return rootDir === '.' ? 'корінь' : rootDir
21
30
  }
22
31
 
32
+ /**
33
+ * Текст кількості файлів українською (1 файл, 2 файли, 5 файлів, 11 файлів).
34
+ * @param {number} n невід’ємна кількість
35
+ * @returns {string} фраза виду «N файл» / «N файли» / «N файлів»
36
+ */
37
+ function ukFilesCountPhrase(n) {
38
+ const m100 = n % 100
39
+ if (m100 >= 11 && m100 <= 14) {
40
+ return `${n} файлів`
41
+ }
42
+ const m10 = n % 10
43
+ if (m10 === 1) {
44
+ return `${n} файл`
45
+ }
46
+ if (m10 >= 2 && m10 <= 4) {
47
+ return `${n} файли`
48
+ }
49
+ return `${n} файлів`
50
+ }
51
+
23
52
  /**
24
53
  * Перевіряє залежності та vite.config одного Vue-пакета.
25
54
  * @param {string} rootDir відносний шлях до пакета
26
55
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
27
- * @returns {Promise<void>} завершується після перевірок залежностей і Vite
56
+ * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
28
57
  */
29
58
  async function checkVuePackage(rootDir, fail) {
30
59
  const label = packageLabel(rootDir)
@@ -95,6 +124,33 @@ async function checkVuePackage(rootDir, fail) {
95
124
  } else {
96
125
  fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
97
126
  }
127
+
128
+ const absPackageRoot = join(process.cwd(), rootDir)
129
+ /** @type {string[]} */
130
+ const sourcePaths = []
131
+ await walkDir(absPackageRoot, absPath => {
132
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
133
+ if (shouldSkipFileForVueImportScan(rel) || !isVueImportScanSourceFile(rel)) {
134
+ return
135
+ }
136
+ sourcePaths.push(absPath)
137
+ })
138
+
139
+ let importViolations = 0
140
+ for (const absPath of sourcePaths) {
141
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
142
+ const content = await readFile(absPath, 'utf8')
143
+ const hits = findForbiddenVueImportsInSourceFile(content, rel)
144
+ for (const v of hits) {
145
+ importViolations++
146
+ fail(`${prefix}${rel}:${v.line} — прибери явний value-імпорт з 'vue' (unplugin-auto-import): ${v.snippet}`)
147
+ }
148
+ }
149
+ if (importViolations === 0) {
150
+ pass(
151
+ `${prefix}немає заборонених value-імпортів з 'vue' у джерелах (проскановано ${ukFilesCountPhrase(sourcePaths.length)})`
152
+ )
153
+ }
98
154
  }
99
155
 
100
156
  /**
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Допоміжні функції для аналізу GitHub Actions workflow (`.yml`) після структурного розбору YAML.
3
+ *
4
+ * Використовується в check-ga, check-js-lint, check-text, check-style-lint, check-npm-module замість
5
+ * пошуку підрядків у сирому тексті там, де важливі лише значення `uses:` та `run:` кроків.
6
+ */
7
+ import { parse } from 'yaml'
8
+
9
+ /**
10
+ * Парсить workflow YAML у звичайний об’єкт; при синтаксичній помилці — `null`.
11
+ * @param {string} content вміст файлу
12
+ * @returns {Record<string, unknown> | null} корінь документа або `null`
13
+ */
14
+ export function parseWorkflowYaml(content) {
15
+ try {
16
+ const root = parse(content)
17
+ return root && typeof root === 'object' ? /** @type {Record<string, unknown>} */ (root) : null
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Збирає всі кроки з усіх jobs.
25
+ * @param {Record<string, unknown>} root корінь workflow
26
+ * @returns {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} плоский список кроків з метаданими
27
+ */
28
+ export function flattenWorkflowSteps(root) {
29
+ const jobs = root?.jobs
30
+ if (!jobs || typeof jobs !== 'object') {
31
+ return []
32
+ }
33
+ /** @type {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} */
34
+ 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
+ }
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ /**
55
+ * Значення `uses:` кроку.
56
+ * @param {Record<string, unknown>} step об’єкт одного елемента `steps`
57
+ * @returns {string} рядок `uses` або порожній рядок
58
+ */
59
+ export function getStepUses(step) {
60
+ return typeof step.uses === 'string' ? step.uses : ''
61
+ }
62
+
63
+ /**
64
+ * Значення `run:` кроку (багаторядковий рядок або масив рядків у YAML).
65
+ * @param {Record<string, unknown>} step об’єкт одного елемента `steps`
66
+ * @returns {string} текст команди
67
+ */
68
+ export function getStepRun(step) {
69
+ const r = step.run
70
+ if (typeof r === 'string') {
71
+ return r
72
+ }
73
+ if (Array.isArray(r)) {
74
+ return r.map(String).join('\n')
75
+ }
76
+ return ''
77
+ }
78
+
79
+ /**
80
+ * Чи є крок, у якого `uses` містить будь-який з підрядків.
81
+ * @param {Record<string, unknown>} root корінь workflow
82
+ * @param {string[]} substrings підрядки для пошуку в `uses`
83
+ * @returns {boolean} `true`, якщо знайдено хоча б один збіг
84
+ */
85
+ export function hasAnyStepUsesContaining(root, substrings) {
86
+ for (const { step } of flattenWorkflowSteps(root)) {
87
+ const uses = getStepUses(step)
88
+ if (substrings.some(s => uses.includes(s))) {
89
+ return true
90
+ }
91
+ }
92
+ return false
93
+ }
94
+
95
+ /**
96
+ * Чи перед першим кроком з локальним `setup-bun-deps` у кожному job є `actions/checkout@`.
97
+ * Якщо `setup-bun-deps` у файлі немає — `true`.
98
+ * @param {Record<string, unknown>} root корінь workflow
99
+ * @param {string[]} setupPathSubstrings підрядки `uses`, що означають локальний composite (наприклад `./.github/actions/setup-bun-deps`)
100
+ * @returns {boolean} `false`, якщо є setup без попереднього checkout
101
+ */
102
+ 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
+ }
134
+ }
135
+ }
136
+ }
137
+ return true
138
+ }
139
+
140
+ /**
141
+ * Шукає заборонені підрядки лише в `uses` та `run` кроків (не в коментарях YAML поза кроками).
142
+ * @param {Record<string, unknown>} root корінь workflow
143
+ * @param {{ pattern: string, msg: string }[]} forbidden список заборонених фрагментів і повідомлень
144
+ * @returns {{ jobId: string, stepIndex: number, pattern: string, msg: string }[]} знайдені збіги
145
+ */
146
+ export function findForbiddenUsesOrRunPatterns(root, forbidden) {
147
+ /** @type {{ jobId: string, stepIndex: number, pattern: string, msg: string }[]} */
148
+ const hits = []
149
+ for (const { jobId, stepIndex, step } of flattenWorkflowSteps(root)) {
150
+ const uses = getStepUses(step)
151
+ const run = getStepRun(step)
152
+ const blob = `${uses}\n${run}`
153
+ for (const { pattern, msg } of forbidden) {
154
+ if (blob.includes(pattern)) {
155
+ hits.push({ jobId, stepIndex, pattern, msg })
156
+ }
157
+ }
158
+ }
159
+ return hits
160
+ }
161
+
162
+ /**
163
+ * Чи є в `on.push.paths` (або `on.pull_request.paths`) елемент з точним значенням.
164
+ * @param {Record<string, unknown>} root корінь workflow
165
+ * @param {'push' | 'pull_request'} event ім’я ключа в `on`
166
+ * @param {string} exact очікуваний glob
167
+ * @returns {boolean} `true`, якщо шлях присутній у масиві `paths`
168
+ */
169
+ export function eventPathsIncludeExact(root, event, exact) {
170
+ const on = root?.on
171
+ if (!on || typeof on !== 'object') {
172
+ return false
173
+ }
174
+ const ev = /** @type {Record<string, unknown>} */ (on)[event]
175
+ if (!ev || typeof ev !== 'object') {
176
+ return false
177
+ }
178
+ const paths = /** @type {Record<string, unknown>} */ (ev).paths
179
+ if (!Array.isArray(paths)) {
180
+ return false
181
+ }
182
+ return paths.includes(exact)
183
+ }
184
+
185
+ /**
186
+ * Чи містить `on.push.paths` підрядок `npm/**` (npm-module).
187
+ * @param {Record<string, unknown>} root корінь workflow
188
+ * @returns {boolean} `true`, якщо серед `paths` є рядок з `npm/**`
189
+ */
190
+ export function pushPathsIncludeNpmGlob(root) {
191
+ const on = root?.on
192
+ if (!on || typeof on !== 'object') {
193
+ return false
194
+ }
195
+ const push = /** @type {Record<string, unknown>} */ (on).push
196
+ if (!push || typeof push !== 'object') {
197
+ return false
198
+ }
199
+ const paths = push.paths
200
+ if (!Array.isArray(paths)) {
201
+ return false
202
+ }
203
+ return paths.some(p => typeof p === 'string' && p.includes('npm/**'))
204
+ }
205
+
206
+ /**
207
+ * Перевіряє наявність `branches` з `main` у `on.push`.
208
+ * @param {Record<string, unknown>} root корінь workflow
209
+ * @returns {boolean} `true`, якщо `main` є в `on.push.branches`
210
+ */
211
+ export function pushHasMainBranch(root) {
212
+ const on = root?.on
213
+ if (!on || typeof on !== 'object') {
214
+ return false
215
+ }
216
+ const push = /** @type {Record<string, unknown>} */ (on).push
217
+ if (!push || typeof push !== 'object') {
218
+ return false
219
+ }
220
+ const branches = push.branches
221
+ if (!Array.isArray(branches)) {
222
+ return false
223
+ }
224
+ return branches.includes('main')
225
+ }
226
+
227
+ /**
228
+ * Чи є крок з `uses: JS-DevTools/npm-publish` та `with.package` для npm-пакета.
229
+ * @param {Record<string, unknown>} root корінь workflow
230
+ * @returns {boolean} `true`, якщо знайдено крок publish з `package: npm/package.json`
231
+ */
232
+ export function hasNpmPublishStepWithPackage(root) {
233
+ for (const { step } of flattenWorkflowSteps(root)) {
234
+ const uses = getStepUses(step)
235
+ if (uses.includes('JS-DevTools/npm-publish')) {
236
+ const w = step.with
237
+ if (w && typeof w === 'object' && /** @type {Record<string, unknown>} */ (w).package === 'npm/package.json') {
238
+ return true
239
+ }
240
+ }
241
+ }
242
+ return false
243
+ }
244
+
245
+ /**
246
+ * Чи є у job `permissions.id-token: write`.
247
+ * @param {Record<string, unknown>} root корінь workflow
248
+ * @returns {boolean} `true`, якщо OIDC-дозвіл для npm publish налаштований
249
+ */
250
+ export function hasIdTokenWritePermission(root) {
251
+ const jobs = root?.jobs
252
+ if (!jobs || typeof jobs !== 'object') {
253
+ return false
254
+ }
255
+ for (const job of Object.values(jobs)) {
256
+ if (job && typeof job === 'object') {
257
+ const perm = /** @type {Record<string, unknown>} */ (job).permissions
258
+ if (perm && typeof perm === 'object' && /** @type {Record<string, unknown>} */ (perm)['id-token'] === 'write') {
259
+ return true
260
+ }
261
+ }
262
+ }
263
+ return false
264
+ }
265
+
266
+ /**
267
+ * Перевірки для `lint-js.yml`: checkout@v6, persist-credentials, setup-bun-deps, run-команди.
268
+ * @param {Record<string, unknown> | null} root корінь workflow або `null` якщо parse не вдався
269
+ * @returns {{ ok: boolean, failures: string[] }} результат перевірки та список причин відмови
270
+ */
271
+ export function verifyLintJsWorkflowStructure(root) {
272
+ /** @type {string[]} */
273
+ const failures = []
274
+ if (!root) {
275
+ return { ok: false, failures: ['YAML не вдалося розібрати — перевір синтаксис workflow'] }
276
+ }
277
+
278
+ const steps = flattenWorkflowSteps(root)
279
+ const usesList = steps.map(s => getStepUses(s.step))
280
+ const runBlob = steps.map(s => getStepRun(s.step)).join('\n')
281
+
282
+ if (!usesList.some(u => u.includes('actions/checkout@v6'))) {
283
+ failures.push('немає кроку uses: actions/checkout@v6')
284
+ }
285
+
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) {
298
+ failures.push('checkout@v6 без with.persist-credentials: false')
299
+ }
300
+
301
+ if (!usesList.some(u => u.includes('./.github/actions/setup-bun-deps'))) {
302
+ failures.push('немає uses: ./.github/actions/setup-bun-deps')
303
+ }
304
+
305
+ if (!runBlob.includes('bunx oxlint')) {
306
+ failures.push('у run немає bunx oxlint')
307
+ }
308
+ if (!runBlob.includes('bunx eslint .')) {
309
+ failures.push('у run немає bunx eslint .')
310
+ }
311
+ if (!runBlob.includes('bunx jscpd .')) {
312
+ failures.push('у run немає bunx jscpd .')
313
+ }
314
+
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
+ }
324
+
325
+ return failures.length === 0 ? { ok: true, failures: [] } : { ok: false, failures }
326
+ }
327
+
328
+ /**
329
+ * Чи є в будь-якому `run` кроку підрядок (наприклад `bun run lint-text`).
330
+ * @param {Record<string, unknown>} root корінь workflow
331
+ * @param {string} needle підрядок для пошуку
332
+ * @returns {boolean} `true`, якщо хоча б один `run` містить `needle`
333
+ */
334
+ export function anyRunStepIncludes(root, needle) {
335
+ for (const { step } of flattenWorkflowSteps(root)) {
336
+ if (getStepRun(step).includes(needle)) {
337
+ return true
338
+ }
339
+ }
340
+ return false
341
+ }
342
+
343
+ /**
344
+ * Чи є в будь-якому `run` підрядок `stylelint`.
345
+ * @param {Record<string, unknown>} root корінь workflow
346
+ * @returns {boolean} `true`, якщо stylelint згадано в команді
347
+ */
348
+ export function anyRunStepIncludesStylelint(root) {
349
+ return anyRunStepIncludes(root, 'stylelint')
350
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Визначає явні імпорти з модуля `vue`, які суперечать vue.mdc (має працювати unplugin-auto-import).
3
+ *
4
+ * Аналіз import виконується через **oxc-parser** (`parseSync`, поле `module.staticImports`) — ESTree-сумісний
5
+ * розбір без евристик по рядках. Дозволені лише side-effect `import 'vue'`, повністю type-only імпорти
6
+ * та `import { type A, type B } from 'vue'` (перевірка `entries[].isType`).
7
+ *
8
+ * Для `.vue` з шаблону витягуються лише теги `<script>` / `<script setup>` (регулярний вираз); далі той самий Oxc-парсинг
9
+ * вмісту скрипта з віртуальним ім’ям `*.ts` для режиму TypeScript.
10
+ */
11
+ import { parseSync } from 'oxc-parser'
12
+
13
+ /**
14
+ * Мова для Oxc за шляхом файлу (розширення).
15
+ * @param {string} filePath віртуальний або реальний шлях до файлу
16
+ * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
17
+ */
18
+ function langFromPath(filePath) {
19
+ const lower = filePath.toLowerCase()
20
+ if (lower.endsWith('.tsx')) {
21
+ return 'tsx'
22
+ }
23
+ if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
24
+ return 'ts'
25
+ }
26
+ if (lower.endsWith('.jsx')) {
27
+ return 'jsx'
28
+ }
29
+ return 'js'
30
+ }
31
+
32
+ /**
33
+ * Номер рядка (1-based) за зміщенням у буфері.
34
+ * @param {string} content повний текст файлу
35
+ * @param {number} offset байтове зміщення початку import
36
+ * @returns {number} номер рядка від 1
37
+ */
38
+ function offsetToLine(content, offset) {
39
+ let line = 1
40
+ const n = Math.min(offset, content.length)
41
+ for (let i = 0; i < n; i++) {
42
+ if (content.codePointAt(i) === 10) {
43
+ line++
44
+ }
45
+ }
46
+ return line
47
+ }
48
+
49
+ /**
50
+ * Стискає пробіли для повідомлення про порушення.
51
+ * @param {string} s фрагмент коду
52
+ * @returns {string} скорочений однорядковий рядок
53
+ */
54
+ function normalizeSnippet(s) {
55
+ return s.replaceAll(/\s+/g, ' ').trim().slice(0, 160)
56
+ }
57
+
58
+ /**
59
+ * Чи цей static import з `vue` дозволено правилом (усі записи type-only або порожній side-effect).
60
+ * @param {{ moduleRequest: { value: string }, entries: { isType: boolean }[] }} imp запис з `module.staticImports`
61
+ * @returns {boolean} `true`, якщо імпорт дозволено (type-only або `import 'vue'`)
62
+ */
63
+ function isAllowedVueStaticImport(imp) {
64
+ if (imp.entries.length === 0) {
65
+ return true
66
+ }
67
+ return imp.entries.every(e => e.isType)
68
+ }
69
+
70
+ /**
71
+ * Віртуальний шлях для парсера: вміст з `<script>` у `.vue` розбираємо як TypeScript.
72
+ * @param {string} relativePath шлях до файлу в пакеті
73
+ * @returns {string} той самий шлях або з `.vue` заміненим на `.ts`
74
+ */
75
+ function virtualPathForParse(relativePath) {
76
+ if (relativePath.endsWith('.vue')) {
77
+ return relativePath.replace(/\.vue$/u, '.ts')
78
+ }
79
+ return relativePath
80
+ }
81
+
82
+ /**
83
+ * Витягує з SFC лише код усередині `<script>`, щоб не чіпати шаблон.
84
+ * @param {string} sfc вміст .vue файлу
85
+ * @returns {string} текст усередині тегів `<script>` (усі блоки поспіль)
86
+ */
87
+ export function extractVueScriptBlocks(sfc) {
88
+ const chunks = []
89
+ const re = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
90
+ let m = re.exec(sfc)
91
+ while (m) {
92
+ chunks.push(m[1])
93
+ m = re.exec(sfc)
94
+ }
95
+ return chunks.join('\n\n')
96
+ }
97
+
98
+ /**
99
+ * Підбирає текст для сканування: для .vue — лише script-блоки, інакше — увесь вміст.
100
+ * @param {string} content сирий вміст файлу
101
+ * @param {string} filePath відносний шлях (для вибору режиму)
102
+ * @returns {string} текст для `parseSync`
103
+ */
104
+ export function contentForVueImportScan(content, filePath) {
105
+ if (filePath.endsWith('.vue')) {
106
+ return extractVueScriptBlocks(content)
107
+ }
108
+ return content
109
+ }
110
+
111
+ /**
112
+ * Знаходить заборонені static import з `vue` у вже підготовленому тексті (без `<template>`).
113
+ * Використовує **oxc-parser**; при синтаксичних помилках повертає порожній масив (спочатку виправ синтаксис).
114
+ * @param {string} content вихідний код
115
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `app/src/foo.ts` або віртуальний після `.vue` → `.ts`)
116
+ * @returns {{ line: number, snippet: string }[]} список порушень з номером рядка початку import
117
+ */
118
+ export function findForbiddenVueImportsInText(content, virtualPath = 'scan.ts') {
119
+ const pathForLang = virtualPath || 'scan.ts'
120
+ const lang = langFromPath(pathForLang)
121
+ let result
122
+ try {
123
+ result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
124
+ } catch {
125
+ return []
126
+ }
127
+ if (result.errors?.length) {
128
+ return []
129
+ }
130
+ /** @type {{ line: number, snippet: string }[]} */
131
+ const out = []
132
+ for (const imp of result.module.staticImports) {
133
+ if (imp.moduleRequest.value === 'vue' && !isAllowedVueStaticImport(imp)) {
134
+ out.push({
135
+ line: offsetToLine(content, imp.start),
136
+ snippet: normalizeSnippet(content.slice(imp.start, imp.end))
137
+ })
138
+ }
139
+ }
140
+ return out
141
+ }
142
+
143
+ /**
144
+ * Чи слід пропустити файл під час обходу пакета (генерація, типи).
145
+ * @param {string} relativePosix шлях з posix-слешами
146
+ * @returns {boolean} `true`, якщо файл не сканувати (`.d.ts`, згенеровані імена)
147
+ */
148
+ export function shouldSkipFileForVueImportScan(relativePosix) {
149
+ const base = relativePosix.split('/').pop() || ''
150
+ if (base === 'auto-imports.d.ts' || base === 'components.d.ts') {
151
+ return true
152
+ }
153
+ if (relativePosix.endsWith('.d.ts')) {
154
+ return true
155
+ }
156
+ return false
157
+ }
158
+
159
+ /**
160
+ * Чи сканувати цей файл за розширенням.
161
+ * @param {string} relativePath відносний шлях до файлу
162
+ * @returns {boolean} `true`, якщо розширення підходить для пошуку import
163
+ */
164
+ export function isVueImportScanSourceFile(relativePath) {
165
+ return /\.(vue|[cm]?[jt]sx?)$/.test(relativePath)
166
+ }
167
+
168
+ /**
169
+ * Знаходить порушення в одному файлі (з урахуванням .vue script extraction).
170
+ * @param {string} content сирий вміст файлу
171
+ * @param {string} relativePath шлях відносно кореня пакета або репо
172
+ * @returns {{ line: number, snippet: string }[]} список порушень для цього файлу
173
+ */
174
+ export function findForbiddenVueImportsInSourceFile(content, relativePath) {
175
+ const scan = contentForVueImportScan(content, relativePath)
176
+ const virtualPath = virtualPathForParse(relativePath)
177
+ return findForbiddenVueImportsInText(scan, virtualPath)
178
+ }