@nitra/cursor 1.13.87 → 1.14.0
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/CHANGELOG.md +48 -0
- package/bin/n-cursor.js +5 -6
- package/package.json +1 -1
- package/rules/abie/abie.mdc +8 -8
- package/rules/abie/js/{applies/check.mjs → applies.mjs} +2 -2
- package/rules/abie/js/{env_dns/check.mjs → env_dns.mjs} +3 -3
- package/rules/abie/js/{firebase_hosting/check.mjs → firebase_hosting.mjs} +1 -1
- package/rules/abie/js/{hc_pairing/check.mjs → hc_pairing.mjs} +4 -4
- package/rules/abie/js/{ua_http_route/check.mjs → ua_http_route.mjs} +6 -6
- package/rules/abie/js/{ua_node_selector/check.mjs → ua_node_selector.mjs} +5 -5
- package/rules/abie/policy/base_deployment_preem/base_deployment_preem.rego +2 -2
- package/rules/abie/policy/health_check_policy/health_check_policy.rego +2 -2
- package/rules/abie/policy/http_route_base/http_route_base.rego +1 -1
- package/rules/abie/utils/enabled.mjs +1 -1
- package/rules/abie/utils/k8s-tree.mjs +1 -1
- package/rules/adr/js/{hooks/check.mjs → hooks.mjs} +1 -1
- package/rules/bun/js/{layout/check.mjs → layout.mjs} +1 -1
- package/rules/capacitor/js/{platforms/check.mjs → platforms.mjs} +1 -1
- package/rules/changelog/changelog.mdc +1 -1
- package/rules/changelog/js/{consistency/check.mjs → consistency.mjs} +2 -2
- package/rules/changelog/{js/consistency → utils}/package-manifest.mjs +1 -1
- package/rules/docker/js/{lint/check.mjs → lint.mjs} +5 -5
- package/rules/docker/lint/lint.mjs +1 -1
- package/rules/docker/{js/lint → utils}/docker-hadolint.mjs +1 -1
- package/rules/feedback/feedback.mdc +1 -1
- package/rules/ga/js/{workflows/check.mjs → workflows.mjs} +5 -5
- package/rules/ga/lint/lint.mjs +1 -1
- package/rules/graphql/js/{tooling/check.mjs → tooling.mjs} +5 -5
- package/rules/hasura/js/{internal_urls/check.mjs → internal_urls.mjs} +4 -4
- package/rules/hasura/policy/svc_hl/svc_hl.rego +1 -1
- package/rules/image-avif/js/{avif_generation/check.mjs → avif_generation.mjs} +5 -5
- package/rules/image-compress/js/{package_setup/check.mjs → package_setup.mjs} +3 -3
- package/rules/js-bun-db/js/{safety/check.mjs → safety.mjs} +5 -5
- package/rules/js-bun-db/{js/safety → utils}/bun-sql-scan.mjs +1 -1
- package/rules/js-bun-redis/js/{imports/check.mjs → imports.mjs} +4 -4
- package/rules/js-bun-redis/policy/package_json/package_json.rego +1 -1
- package/rules/js-lint/js/{tooling/check.mjs → tooling.mjs} +8 -4
- package/rules/js-lint/js-lint.mdc +11 -1
- package/rules/js-lint/{js/tooling → utils}/rebuild-oxlint-canonical.mjs +2 -2
- package/rules/js-mssql/js/{deps/check.mjs → deps.mjs} +5 -5
- package/rules/js-mssql/{js/deps → utils}/mssql-pool-scan.mjs +1 -1
- package/rules/js-run/js/{runtime/check.mjs → runtime.mjs} +10 -10
- package/rules/js-run/{js/runtime → utils}/bunyan-imports.mjs +1 -1
- package/rules/js-run/{js/runtime → utils}/check-env-scan.mjs +1 -1
- package/rules/js-run/{js/runtime → utils}/conn-file-rules.mjs +1 -1
- package/rules/js-run/{js/runtime → utils}/conn-imports-scan.mjs +1 -1
- package/rules/js-run/{js/runtime → utils}/promise-settimeout-scan.mjs +1 -1
- package/rules/k8s/js/{manifests/check.mjs → manifests.mjs} +4 -4
- package/rules/k8s/k8s.mdc +1 -1
- package/rules/nginx-default-tpl/js/{template/check.mjs → template.mjs} +5 -5
- package/rules/npm-module/js/{package_structure/check.mjs → package_structure.mjs} +4 -4
- package/rules/php/js/{tooling/check.mjs → tooling.mjs} +1 -1
- package/rules/rego/js/{applies/check.mjs → applies.mjs} +3 -3
- package/rules/security/js/{sample_secret/check.mjs → sample_secret.mjs} +2 -2
- package/rules/security/js/{trufflehog/check.mjs → trufflehog.mjs} +3 -3
- package/rules/security/security.mdc +2 -2
- package/rules/style-lint/js/{tooling/check.mjs → tooling.mjs} +1 -1
- package/rules/tauri/js/{tooling/check.mjs → tooling.mjs} +2 -2
- package/rules/test/js/{location/check.mjs → location.mjs} +3 -3
- package/rules/text/js/{formatting/check.mjs → formatting.mjs} +2 -2
- package/rules/vue/js/{packages/check.mjs → packages.mjs} +5 -5
- package/scripts/auto-rules.mjs +3 -3
- package/scripts/claude-stop-hook.mjs +2 -2
- package/scripts/sync-claude-config.mjs +5 -4
- package/scripts/utils/discover-checkable-rules.mjs +20 -27
- package/scripts/utils/inline-template-links.mjs +1 -1
- package/scripts/utils/run-rule.mjs +18 -23
- package/scripts/utils/with-lock.mjs +24 -12
- package/.claude-template/commands/n-check.md +0 -11
- /package/rules/adr/js/{hooks/template → templates/hooks}/.gitignore.snippet +0 -0
- /package/rules/docker/{js/lint → utils}/docker-mirror.mjs +0 -0
- /package/rules/graphql/{js/tooling → utils}/graphql-gql-scan.mjs +0 -0
- /package/rules/js-lint/js/{tooling → data/tooling}/knip-canonical.json +0 -0
- /package/rules/js-lint/js/{tooling → data/tooling}/oxlint-canonical-skeleton.json +0 -0
- /package/rules/js-lint/js/{tooling → data/tooling}/oxlint-canonical.json +0 -0
- /package/rules/js-lint/js/{tooling → data/tooling}/oxlint-rules.tsv +0 -0
- /package/rules/k8s/js/{kubescape_exceptions/template → templates/kubescape_exceptions}/.kubescape-exceptions.json.snippet.json +0 -0
- /package/rules/security/js/{trufflehog/template → templates/trufflehog}/.trufflehog-exclude.snippet.txt +0 -0
- /package/rules/vue/{js/packages → utils}/vue-forbidden-imports.mjs +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Канон надходить через --data: { "template": { "deny": ... } }
|
|
4
4
|
# Структура --data сформована з template/package.json.deny.json.
|
|
5
5
|
# AST-скан коду (`import`/`require`/dynamic `import()` тих самих пакетів)
|
|
6
|
-
# лишається у `js-bun-redis/js/imports
|
|
6
|
+
# лишається у `js-bun-redis/js/imports.mjs`.
|
|
7
7
|
package js_bun_redis.package_json
|
|
8
8
|
|
|
9
9
|
import rego.v1
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
4
|
* Flat ESLint з getConfig і ignore для auto-imports,
|
|
5
|
-
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/rules/js-lint/js/tooling/oxlint-canonical.json`):
|
|
5
|
+
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/rules/js-lint/js/data/tooling/oxlint-canonical.json`):
|
|
6
6
|
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
7
|
* globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
|
|
8
8
|
* і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
|
|
@@ -15,17 +15,21 @@ import { copyFile, readFile } from 'node:fs/promises'
|
|
|
15
15
|
import { dirname, join } from 'node:path'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
17
|
|
|
18
|
-
import { createCheckReporter } from '
|
|
18
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
19
19
|
|
|
20
20
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
21
|
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
22
|
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'data',
|
|
24
|
+
'tooling',
|
|
23
25
|
'oxlint-canonical.json'
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
/** Шлях до канонічного knip JSON у цьому пакеті — копіюється у корінь проєкту-споживача, якщо відсутній. */
|
|
27
29
|
export const KNIP_CANONICAL_JSON_PATH = join(
|
|
28
30
|
dirname(fileURLToPath(import.meta.url)),
|
|
31
|
+
'data',
|
|
32
|
+
'tooling',
|
|
29
33
|
'knip-canonical.json'
|
|
30
34
|
)
|
|
31
35
|
|
|
@@ -154,7 +158,7 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
|
154
158
|
|
|
155
159
|
if (!deepEqualOxlintCanonical(actual, expected)) {
|
|
156
160
|
failures.push(
|
|
157
|
-
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js-lint/js/tooling/oxlint-canonical.json)`
|
|
161
|
+
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js-lint/js/data/tooling/oxlint-canonical.json)`
|
|
158
162
|
)
|
|
159
163
|
}
|
|
160
164
|
}
|
|
@@ -384,7 +388,7 @@ async function checkKnipConfig(passFn, failFn) {
|
|
|
384
388
|
return
|
|
385
389
|
}
|
|
386
390
|
await copyFile(KNIP_CANONICAL_JSON_PATH, 'knip.json')
|
|
387
|
-
passFn('knip.json створено з канонічного npm/rules/js-lint/js/tooling/knip-canonical.json (js-lint.mdc)')
|
|
391
|
+
passFn('knip.json створено з канонічного npm/rules/js-lint/js/data/tooling/knip-canonical.json (js-lint.mdc)')
|
|
388
392
|
}
|
|
389
393
|
|
|
390
394
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
globs: "**/{.oxlintrc.json,eslint.config.js,.jscpd.json,knip.json,package.json},**/*.{js,mjs,cjs,jsx,ts,tsx}"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.24'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
**oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd/knip не додавай без потреби монорепо.
|
|
@@ -79,6 +79,16 @@ version: '1.23'
|
|
|
79
79
|
|
|
80
80
|
Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
|
|
81
81
|
|
|
82
|
+
## Структура спільних модулів: `utils/` vs `lib/`
|
|
83
|
+
|
|
84
|
+
Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
|
|
85
|
+
|
|
86
|
+
- **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
|
|
87
|
+
|
|
88
|
+
- **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
|
|
89
|
+
|
|
90
|
+
Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
|
|
91
|
+
|
|
82
92
|
Додай workflow:
|
|
83
93
|
|
|
84
94
|
```yaml title=".github/workflows/lint-js.yml"
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
|
|
3
3
|
* правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
|
|
4
4
|
*
|
|
5
|
-
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/
|
|
5
|
+
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/utils/rebuild-oxlint-canonical.mjs`,
|
|
6
6
|
* потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { dirname, join } from 'node:path'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
const dir = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)), '..', 'js', 'data', 'tooling')
|
|
13
13
|
const rules = {}
|
|
14
14
|
for (const line of readFileSync(join(dir, 'oxlint-rules.tsv'), 'utf8').split('\n')) {
|
|
15
15
|
const t = line.trim()
|
|
@@ -12,8 +12,8 @@ import { existsSync } from 'node:fs'
|
|
|
12
12
|
import { readFile } from 'node:fs/promises'
|
|
13
13
|
import { join, relative } from 'node:path'
|
|
14
14
|
|
|
15
|
-
import { createCheckReporter } from '
|
|
16
|
-
import { findAllPackageJsonPaths } from '
|
|
15
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
16
|
+
import { findAllPackageJsonPaths } from '../../../scripts/utils/find-package-json-paths.mjs'
|
|
17
17
|
import {
|
|
18
18
|
findMssqlPerRequestConnectionInText,
|
|
19
19
|
findSharedMssqlRequestInText,
|
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
findUnsafeMssqlInListUnparsedInText,
|
|
23
23
|
findUnsafeMssqlInListMissingEmptyGuardInText,
|
|
24
24
|
isMssqlScanSourceFile
|
|
25
|
-
} from '
|
|
26
|
-
import { loadCursorIgnorePaths } from '
|
|
27
|
-
import { walkDir } from '
|
|
25
|
+
} from '../utils/mssql-pool-scan.mjs'
|
|
26
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
27
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
28
28
|
|
|
29
29
|
const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/u
|
|
30
30
|
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/u
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
normalizeSnippet,
|
|
31
31
|
offsetToLine,
|
|
32
32
|
walkAstWithAncestors
|
|
33
|
-
} from '
|
|
33
|
+
} from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
34
34
|
|
|
35
35
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
36
36
|
const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
|
|
@@ -40,24 +40,24 @@ import {
|
|
|
40
40
|
findBunyanImportsInText,
|
|
41
41
|
isBunyanScanSourceFile,
|
|
42
42
|
shouldSkipFileForBunyanScan
|
|
43
|
-
} from '
|
|
44
|
-
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '
|
|
45
|
-
import { createCheckReporter } from '
|
|
46
|
-
import { runConftestBatch } from '
|
|
47
|
-
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '
|
|
43
|
+
} from '../utils/bunyan-imports.mjs'
|
|
44
|
+
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '../utils/check-env-scan.mjs'
|
|
45
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
46
|
+
import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
|
|
47
|
+
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '../utils/conn-file-rules.mjs'
|
|
48
48
|
import {
|
|
49
49
|
findConnFactoryImportsInText,
|
|
50
50
|
isConnImportsScanSourceFile,
|
|
51
51
|
isInsideConnDir,
|
|
52
52
|
resolveConnDirFromPackageJson
|
|
53
|
-
} from '
|
|
54
|
-
import { loadCursorIgnorePaths } from '
|
|
53
|
+
} from '../utils/conn-imports-scan.mjs'
|
|
54
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
55
55
|
import {
|
|
56
56
|
findPromiseSetTimeoutInText,
|
|
57
57
|
isPromiseSetTimeoutScanSourceFile
|
|
58
|
-
} from '
|
|
59
|
-
import { walkDir } from '
|
|
60
|
-
import { getMonorepoPackageRootDirs } from '
|
|
58
|
+
} from '../utils/promise-settimeout-scan.mjs'
|
|
59
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
60
|
+
import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
offsetToLine,
|
|
19
19
|
requireCallModule,
|
|
20
20
|
walkAstWithAncestors
|
|
21
|
-
} from '
|
|
21
|
+
} from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
22
22
|
|
|
23
23
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
24
24
|
const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
|
|
30
30
|
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
31
31
|
*/
|
|
32
|
-
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '
|
|
32
|
+
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
33
33
|
|
|
34
34
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
35
35
|
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Парсимо через oxc-parser; коли файл не парситься — повертаємо порожні результати, щоб
|
|
15
15
|
* не змішувати помилки синтаксису з порушеннями цього правила.
|
|
16
16
|
*/
|
|
17
|
-
import { parseProgramOrNull } from '
|
|
17
|
+
import { parseProgramOrNull } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
18
18
|
|
|
19
19
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
20
20
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* використовується. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
17
17
|
* треба полагодити синтаксис.
|
|
18
18
|
*/
|
|
19
|
-
import { langFromPath, normalizeSnippet, offsetToLine } from '
|
|
19
|
+
import { langFromPath, normalizeSnippet, offsetToLine } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
20
20
|
import { parseSync } from 'oxc-parser'
|
|
21
21
|
|
|
22
22
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
|
|
13
13
|
* порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
|
|
14
14
|
*/
|
|
15
|
-
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '
|
|
15
|
+
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
16
16
|
|
|
17
17
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
18
18
|
|
|
@@ -140,10 +140,10 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
|
140
140
|
|
|
141
141
|
import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
|
|
142
142
|
|
|
143
|
-
import { createCheckReporter } from '
|
|
144
|
-
import { loadCursorIgnorePaths } from '
|
|
145
|
-
import { runConftestBatch } from '
|
|
146
|
-
import { walkDir } from '
|
|
143
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
144
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
145
|
+
import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
|
|
146
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
147
147
|
|
|
148
148
|
/** Версія набору схем yannh — узгоджено з k8s.mdc */
|
|
149
149
|
const YAML_LS_MODELINE_RE = /^# yaml-language-server: \$schema=.*\n/
|
package/rules/k8s/k8s.mdc
CHANGED
|
@@ -39,7 +39,7 @@ alwaysApply: false
|
|
|
39
39
|
|
|
40
40
|
Якщо в **корені проєкту** є файл **`.kubescape-exceptions.json`** — `lint-k8s` автоматично передає його в `kubescape scan` через **`--exceptions`** ([postureExceptionPolicy](https://github.com/kubescape/kubescape/blob/master/docs/exceptions.md)). Файл — JSON-масив об'єктів з полями `name`, `policyType: "postureExceptionPolicy"`, `actions` (`["alertOnly"]` — знижує fail до alert, не блокує lint), `resources` (resource designator) і `posturePolicies` (масив `controlID`).
|
|
41
41
|
|
|
42
|
-
Канонічний кейс — **C-0012** (`Applications credentials in configuration files`, High): control тригериться на **імʼя** env, що містить підрядок `secret`/`password`/`key`/`token`, а **не** на значення. Для `HASURA_GRAPHQL_JWT_SECRET` у ConfigMap значення — публічний JWT-конфіг (`jwk_url`, `issuer`), не credentials, але kubescape падає лише через імʼя. Точкове виключення для ConfigMap із цим env — канон: [.kubescape-exceptions.json.snippet.json](./js/kubescape_exceptions
|
|
42
|
+
Канонічний кейс — **C-0012** (`Applications credentials in configuration files`, High): control тригериться на **імʼя** env, що містить підрядок `secret`/`password`/`key`/`token`, а **не** на значення. Для `HASURA_GRAPHQL_JWT_SECRET` у ConfigMap значення — публічний JWT-конфіг (`jwk_url`, `issuer`), не credentials, але kubescape падає лише через імʼя. Точкове виключення для ConfigMap із цим env — канон: [.kubescape-exceptions.json.snippet.json](./js/templates/kubescape_exceptions/.kubescape-exceptions.json.snippet.json)
|
|
43
43
|
|
|
44
44
|
Підстав свою `attributes.name` (рядок або regex), якщо ConfigMap зветься інакше; виключай контрольно, а не глобально (не додавай винятки без `attributes.name`/`labels`, бо тоді C-0012 знімається для усіх ConfigMap-ів проєкту і реальні витоки credentials теж пройдуть).
|
|
45
45
|
|
|
@@ -17,11 +17,11 @@ import { existsSync } from 'node:fs'
|
|
|
17
17
|
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
18
18
|
import { basename, dirname, join, relative } from 'node:path'
|
|
19
19
|
|
|
20
|
-
import { findDockerfilePaths } from '
|
|
21
|
-
import { createCheckReporter } from '
|
|
22
|
-
import { loadCursorIgnorePaths } from '
|
|
23
|
-
import { runConftestBatch } from '
|
|
24
|
-
import { walkDir } from '
|
|
20
|
+
import { findDockerfilePaths } from '../../docker/js/lint.mjs'
|
|
21
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
22
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
23
|
+
import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
|
|
24
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
25
25
|
|
|
26
26
|
const LINE_SPLIT_RE = /\r?\n/u
|
|
27
27
|
const INI_KEY_RE = /^([A-Za-z_]\w*)\s*=/u
|
|
@@ -37,10 +37,10 @@ import {
|
|
|
37
37
|
langFromPath,
|
|
38
38
|
requireCallModule,
|
|
39
39
|
walkAstWithAncestors
|
|
40
|
-
} from '
|
|
41
|
-
import { createCheckReporter } from '
|
|
42
|
-
import { loadCursorIgnorePaths } from '
|
|
43
|
-
import { walkDir } from '
|
|
40
|
+
} from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
41
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
42
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
43
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
44
44
|
|
|
45
45
|
const execFileAsync = promisify(execFile)
|
|
46
46
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync } from 'node:fs'
|
|
13
13
|
|
|
14
|
-
import { createCheckReporter } from '
|
|
14
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Перевіряє відповідність проєкту правилам php.mdc.
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* JS тут лишається лише як cross-file гейт: walkDir не виразити декларативно через `target.json`.
|
|
10
10
|
* Друк короткого pass-повідомлення з контекстом робить `check()` (необовʼязковий).
|
|
11
11
|
*/
|
|
12
|
-
import { createCheckReporter } from '
|
|
13
|
-
import { loadCursorIgnorePaths } from '
|
|
14
|
-
import { walkDir } from '
|
|
12
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
13
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
14
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Чи є хоча б один `.rego`-файл у дереві від `cwd`. Зупиняється на першому матчі.
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
import { readFile } from 'node:fs/promises'
|
|
30
30
|
import { relative, sep } from 'node:path'
|
|
31
31
|
|
|
32
|
-
import { createCheckReporter } from '
|
|
33
|
-
import { walkDir } from '
|
|
32
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
33
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
34
34
|
|
|
35
35
|
/** Суфікс basename'а прикладного файлу (`config.example`, `.env.dist`). */
|
|
36
36
|
const EXAMPLE_SUFFIX_RE = /\.(?:example|sample|template|dist)$/iu
|
|
@@ -11,11 +11,11 @@ import { readFile } from 'node:fs/promises'
|
|
|
11
11
|
import { dirname, join } from 'node:path'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
import { createCheckReporter } from '
|
|
15
|
-
import { checkTextSubset } from '
|
|
14
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
15
|
+
import { checkTextSubset } from '../../../scripts/utils/template.mjs'
|
|
16
16
|
|
|
17
17
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
18
|
-
const SNIPPET_PATH = join(HERE, '
|
|
18
|
+
const SNIPPET_PATH = join(HERE, 'templates', 'trufflehog', '.trufflehog-exclude.snippet.txt')
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @returns {Promise<number>} exit-код перевірки
|
|
@@ -24,7 +24,7 @@ version: '2.1'
|
|
|
24
24
|
|
|
25
25
|
## `.trufflehog-exclude` (рекомендована основа)
|
|
26
26
|
|
|
27
|
-
Канон (допускає розширення): [.trufflehog-exclude.snippet.txt](./js/trufflehog
|
|
27
|
+
Канон (допускає розширення): [.trufflehog-exclude.snippet.txt](./js/templates/trufflehog/.trufflehog-exclude.snippet.txt)
|
|
28
28
|
|
|
29
29
|
**Важливо:** один regex-pattern на рядок, без TOML-обгортки; коментарі починаються з `#`.
|
|
30
30
|
|
|
@@ -45,7 +45,7 @@ Workflow обовʼязковий — забезпечує незалежний
|
|
|
45
45
|
- Правильно: `DB_PASSWORD=sample-secret`, `password: "sample-secret"`
|
|
46
46
|
- Неправильно: `DB_PASSWORD=secret`, `password: "secret"`
|
|
47
47
|
|
|
48
|
-
Перевіряється лише `secret` у позиції значення (після `=`, `:`, `=>`); імена ключів на кшталт `client_secret` не чіпаються. Concern `security.sample_secret` — деталі скану в `js/sample_secret
|
|
48
|
+
Перевіряється лише `secret` у позиції значення (після `=`, `:`, `=>`); імена ключів на кшталт `client_secret` не чіпаються. Concern `security.sample_secret` — деталі скану в `js/sample_secret.mjs`.
|
|
49
49
|
|
|
50
50
|
## Перевірка
|
|
51
51
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
20
|
import { readFile } from 'node:fs/promises'
|
|
21
21
|
|
|
22
|
-
import { createCheckReporter } from '
|
|
22
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Альтернатива полю `stylelint` у `package.json` — зовнішній файл конфігу. Якщо
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
import { existsSync, statSync } from 'node:fs'
|
|
19
19
|
import { readFile } from 'node:fs/promises'
|
|
20
20
|
|
|
21
|
-
import { createCheckReporter } from '
|
|
22
|
-
import { runConftestBatch } from '
|
|
21
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
22
|
+
import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Чи є префікс `@tauri-apps/` у ключах `dependencies` або `devDependencies`.
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { basename, dirname, relative } from 'node:path'
|
|
11
11
|
|
|
12
|
-
import { createCheckReporter } from '
|
|
13
|
-
import { loadCursorIgnorePaths } from '
|
|
14
|
-
import { walkDir } from '
|
|
12
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
13
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
14
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
15
15
|
|
|
16
16
|
const TESTS_DIR_NAME = 'tests'
|
|
17
17
|
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
import { existsSync } from 'node:fs'
|
|
33
33
|
import { readFile } from 'node:fs/promises'
|
|
34
34
|
|
|
35
|
-
import { createCheckReporter } from '
|
|
36
|
-
import { anyRunStepIncludes, parseWorkflowYaml } from '
|
|
35
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
36
|
+
import { anyRunStepIncludes, parseWorkflowYaml } from '../../../scripts/utils/gha-workflow.mjs'
|
|
37
37
|
|
|
38
38
|
/** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
|
|
39
39
|
const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
|
|
@@ -25,16 +25,16 @@ import { existsSync } from 'node:fs'
|
|
|
25
25
|
import { readFile } from 'node:fs/promises'
|
|
26
26
|
import { join, relative } from 'node:path'
|
|
27
27
|
|
|
28
|
-
import { createCheckReporter } from '
|
|
28
|
+
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
29
29
|
import {
|
|
30
30
|
findForbiddenNodeImportsInVueFile,
|
|
31
31
|
findForbiddenVueImportsInSourceFile,
|
|
32
32
|
isVueImportScanSourceFile,
|
|
33
33
|
shouldSkipFileForVueImportScan
|
|
34
|
-
} from '
|
|
35
|
-
import { loadCursorIgnorePaths } from '
|
|
36
|
-
import { walkDir } from '
|
|
37
|
-
import { getMonorepoPackageRootDirs } from '
|
|
34
|
+
} from '../utils/vue-forbidden-imports.mjs'
|
|
35
|
+
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
36
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
37
|
+
import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
|
|
38
38
|
|
|
39
39
|
const ESBUILD_RE = /\besbuild\b/
|
|
40
40
|
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -16,13 +16,13 @@ import { existsSync } from 'node:fs'
|
|
|
16
16
|
import { readdir, readFile } from 'node:fs/promises'
|
|
17
17
|
import { basename, join, relative } from 'node:path'
|
|
18
18
|
|
|
19
|
-
import { textHasBunSqlImport } from '../rules/js-bun-db/
|
|
19
|
+
import { textHasBunSqlImport } from '../rules/js-bun-db/utils/bun-sql-scan.mjs'
|
|
20
20
|
import {
|
|
21
21
|
isGqlScanSourceFile,
|
|
22
22
|
shouldSkipFileForGqlScan,
|
|
23
23
|
sourceFileHasGqlTaggedTemplate
|
|
24
|
-
} from '../rules/graphql/
|
|
25
|
-
import { contentForVueImportScan } from '../rules/vue/
|
|
24
|
+
} from '../rules/graphql/utils/graphql-gql-scan.mjs'
|
|
25
|
+
import { contentForVueImportScan } from '../rules/vue/utils/vue-forbidden-imports.mjs'
|
|
26
26
|
|
|
27
27
|
/** Порядок автододавання правил відповідно до `rules/<rule>/auto.md`. */
|
|
28
28
|
export const AUTO_RULE_ORDER = Object.freeze([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stop-hook для Claude Code: запускається hook'ом із `.claude/settings.json` після того,
|
|
3
|
-
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor
|
|
3
|
+
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor fix`
|
|
4
4
|
* і повертає його exit code, щоб помилки правил блокували завершення.
|
|
5
5
|
*
|
|
6
6
|
* Захист від нескінченної рекурсії: якщо stdin містить `"stop_hook_active": true`
|
|
@@ -63,7 +63,7 @@ export async function runStopHookCli() {
|
|
|
63
63
|
return 0
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const child = spawn('npx', ['--no', '@nitra/cursor', '
|
|
66
|
+
const child = spawn('npx', ['--no', '@nitra/cursor', 'fix'], { stdio: 'inherit' })
|
|
67
67
|
try {
|
|
68
68
|
const [code] = await once(child, 'exit')
|
|
69
69
|
return code ?? 1
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Синхронізує конфігурацію Claude Code (`.claude/settings.json`,
|
|
3
|
-
* slash-команди
|
|
4
|
-
* у поточний проєкт із темплейтів пакету
|
|
3
|
+
* slash-команди з `commands/` темплейту, ADR Stop-hook) і Cursor hooks
|
|
4
|
+
* (`.cursor/hooks.json`) у поточний проєкт із темплейтів пакету
|
|
5
5
|
* `npm/.claude-template/`.
|
|
6
6
|
*
|
|
7
7
|
* Архітектура:
|
|
8
8
|
* - `settings.json` — **merge**: користувацькі поля зберігаються; наші hooks
|
|
9
9
|
* ідентифікуються командою-маркером (`MANAGED_HOOK_COMMAND_MARKERS`) і
|
|
10
10
|
* перезаписуються; permissions.allow зливається через union (із дедублікацією).
|
|
11
|
-
* - `.claude/commands
|
|
11
|
+
* - `.claude/commands/*.md` — fully owned slash-команди з темплейту
|
|
12
|
+
* `.claude-template/commands/` (зараз порожньо; sync no-op).
|
|
12
13
|
* - `.claude/hooks/capture-decisions.sh` — fully owned bash-скрипт ADR capture Stop-hook;
|
|
13
14
|
* копіюється з `.claude-template/hooks/`, лише коли в `.n-cursor.json` `rules`
|
|
14
15
|
* присутнє `adr` (правило увімкнене за замовчуванням; вимикається через
|
|
@@ -56,7 +57,7 @@ const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
|
56
57
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
57
58
|
const TEMPLATE_DIR_NAME = '.claude-template'
|
|
58
59
|
/** Відносний шлях до канонічного фрагмента `.gitignore` для ADR Stop-hook'ів у tarball пакета. */
|
|
59
|
-
export const ADR_GITIGNORE_SNIPPET_REL = 'rules/adr/js/hooks
|
|
60
|
+
export const ADR_GITIGNORE_SNIPPET_REL = 'rules/adr/js/templates/hooks/.gitignore.snippet'
|
|
60
61
|
const GITIGNORE_FILE = '.gitignore'
|
|
61
62
|
const EOL_RE = /\r?\n/u
|
|
62
63
|
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Discovery rules для CLI `
|
|
3
|
-
* - JS concerns: `rules/<id>/js/<concern
|
|
2
|
+
* Discovery rules для CLI `fix`. Шукає правила, для яких є щось «прогонне»:
|
|
3
|
+
* - JS concerns: `rules/<id>/js/<concern>.mjs` — один файл = один concern.
|
|
4
4
|
* - Policy concerns: `rules/<id>/policy/<concern>/target.json` — пара з `<concern>.rego`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* Файли `*.test.mjs` фільтруються regex (`^check(?:-.+)?\.mjs$`).
|
|
6
|
+
* Файли з префіксом `_` (зокрема каталог `_lib/`) і `*.test.mjs` пропускаються — це хелпери й тести.
|
|
8
7
|
*
|
|
9
8
|
* Намеренно НЕ парсимо `target.json` тут (це робить runner). Discovery — швидкий скан структури:
|
|
10
9
|
* шляхи + назви, без I/O вмісту.
|
|
11
10
|
*
|
|
12
|
-
* Історичний контекст: convention пройшла еволюцію
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Історичний контекст: convention пройшла еволюцію
|
|
12
|
+
* `js/<concern>/check.mjs` (1.13.80–1.13.89)
|
|
13
|
+
* → `js/<concern>.mjs` (1.13.90+, flat: концерн = файл, не каталог)
|
|
14
|
+
* Helpers, tests, templates і data винесені в окремі топ-level папки правила (`js/_lib/`,
|
|
15
|
+
* `tests/`, `templates/`, `data/`).
|
|
15
16
|
*/
|
|
16
17
|
import { existsSync } from 'node:fs'
|
|
17
18
|
import { readdir } from 'node:fs/promises'
|
|
18
19
|
import { join } from 'node:path'
|
|
19
20
|
|
|
20
|
-
const CHECK_FILENAME_RE = /^check(?:-.+)?\.mjs$/u
|
|
21
|
-
const TEST_SUFFIX = '.test.mjs'
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* @typedef {object} JsConcern
|
|
25
|
-
* @property {string} name імʼя концерну (
|
|
26
|
-
* @property {string[]} files імена `check*.mjs` у концерні (відсортовані алфавітно)
|
|
23
|
+
* @property {string} name імʼя концерну (= basename файла `js/<name>.mjs` без розширення)
|
|
27
24
|
*/
|
|
28
25
|
|
|
29
26
|
/**
|
|
@@ -39,30 +36,26 @@ const TEST_SUFFIX = '.test.mjs'
|
|
|
39
36
|
*/
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
|
-
* Перелічує JS-концерни одного правила:
|
|
39
|
+
* Перелічує JS-концерни одного правила: файли `js/<name>.mjs` (один файл — один concern).
|
|
43
40
|
*
|
|
44
|
-
*
|
|
41
|
+
* Файли з префіксом `_` (наприклад каталог `_lib/`) і `*.test.mjs` пропускаються.
|
|
45
42
|
* @param {string} jsDir абсолютний шлях `rules/<id>/js/`
|
|
46
43
|
* @returns {Promise<JsConcern[]>} концерни в алфавітному порядку
|
|
47
44
|
*/
|
|
48
45
|
async function listJsConcerns(jsDir) {
|
|
49
46
|
if (!existsSync(jsDir)) return []
|
|
50
|
-
const
|
|
51
|
-
|
|
47
|
+
const entries = await readdir(jsDir, { withFileTypes: true })
|
|
52
48
|
/** @type {JsConcern[]} */
|
|
53
49
|
const concerns = []
|
|
54
|
-
for (const entry of
|
|
55
|
-
if (!entry.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
concerns.push({ name: entry.name, files })
|
|
63
|
-
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isFile()) continue
|
|
52
|
+
if (!entry.name.endsWith('.mjs')) continue
|
|
53
|
+
if (entry.name.endsWith('.test.mjs')) continue
|
|
54
|
+
if (entry.name.startsWith('_')) continue
|
|
55
|
+
if (entry.name.startsWith('.')) continue
|
|
56
|
+
const name = entry.name.slice(0, -'.mjs'.length)
|
|
57
|
+
concerns.push({ name })
|
|
64
58
|
}
|
|
65
|
-
|
|
66
59
|
return concerns.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
67
60
|
}
|
|
68
61
|
|
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { basename, extname, join } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
|
|
6
|
-
const TEMPLATE_SEGMENT_RE = /\/
|
|
6
|
+
const TEMPLATE_SEGMENT_RE = /\/templates?\//
|
|
7
7
|
/** Статичні regexp-літерали `^(.+)\.<slot>\.<ext>$` — без `RegExp(variable)`. */
|
|
8
8
|
const SLOT_SUFFIX_RES = [/^(.+)\.snippet\.[^.]+$/, /^(.+)\.deny\.[^.]+$/, /^(.+)\.contains\.[^.]+$/]
|
|
9
9
|
|