@7n/rules-lang-js 0.9.0 → 0.10.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 +13 -0
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.mdc +3 -1
- package/rules/bun/package_json/package_json.rego +30 -1
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
- package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
- package/rules/storybook/adopt/docs/index.md +9 -0
- package/rules/storybook/adopt/docs/main.md +46 -0
- package/rules/storybook/adopt/main.mjs +379 -0
- package/rules/storybook/hygiene/concern.json +4 -0
- package/rules/storybook/hygiene/docs/index.md +9 -0
- package/rules/storybook/hygiene/docs/main.md +45 -0
- package/rules/storybook/hygiene/main.mjs +254 -0
- package/rules/storybook/main.json +1 -0
- package/rules/storybook/main.mdc +60 -0
- package/rules/storybook/mocking/concern.json +3 -0
- package/rules/storybook/mocking/mocking.mdc +108 -0
- package/rules/storybook/scaffold/concern.json +5 -0
- package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
- package/rules/storybook/scaffold/docs/index.md +10 -0
- package/rules/storybook/scaffold/docs/main.md +36 -0
- package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
- package/rules/storybook/scaffold/main.mjs +164 -0
- package/rules/storybook/scaffold/template/docs/index.md +10 -0
- package/rules/storybook/scaffold/template/docs/main.md +30 -0
- package/rules/storybook/scaffold/template/docs/preview.md +46 -0
- package/rules/storybook/scaffold/template/main.js +45 -0
- package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
- package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
- package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
- package/rules/storybook/scaffold/template/preview.js +47 -0
- package/rules/storybook/scope/concern.json +4 -0
- package/rules/storybook/scope/docs/index.md +9 -0
- package/rules/storybook/scope/docs/main.md +62 -0
- package/rules/storybook/scope/main.mjs +202 -0
- package/rules/storybook/vitest-config/concern.json +8 -0
- package/rules/storybook/vitest-config/docs/fix-vitest-config.md +38 -0
- package/rules/storybook/vitest-config/docs/index.md +10 -0
- package/rules/storybook/vitest-config/docs/main.md +72 -0
- package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
- package/rules/storybook/vitest-config/main.mjs +358 -0
- package/rules/storybook/vitest-config/template/docs/index.md +12 -0
- package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -0
- package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
- package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +29 -0
- package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
- package/rules/storybook/vitest-config/template/storybook-project-entry.js +22 -0
- package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
- package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
- package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
- package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-autofix для concern-а `storybook/scaffold`: відтворює канонічні `.storybook/main.js`,
|
|
3
|
+
* `.storybook/preview.js`, `.storybook/mocks/gql-sse.js` і `package.json#scripts.storybook`
|
|
4
|
+
* з `template/` цього concern-а. `main.js` має одну заміну на пакет — stories-glob за
|
|
5
|
+
* layout-детекцією (`detectStoriesGlob`, `main.mjs`); `preview.js`/`mocks/gql-sse.js` —
|
|
6
|
+
* verbatim-копія (Quasar/msw-налаштування не залежать від конкретного пакета).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
import { detectStoriesGlob, STORYBOOK_SCRIPT } from './main.mjs'
|
|
13
|
+
|
|
14
|
+
const STORIES_GLOB_TOKEN = '__STORYBOOK_STORIES_GLOB__'
|
|
15
|
+
|
|
16
|
+
/** Каталог `template/` цього concern-а. Експортовано — переюз у `adopt/main.mjs`. */
|
|
17
|
+
export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Рендерить канонічний `.storybook/main.js` для конкретного пакета (єдина заміна —
|
|
21
|
+
* stories-glob за layout-детекцією). Експортовано — той самий рендер переюзає
|
|
22
|
+
* `adopt/main.mjs` для генерації повністю відсутнього файлу (не дублювати шаблонування).
|
|
23
|
+
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
24
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
25
|
+
* @returns {string} готовий вміст `main.js`
|
|
26
|
+
*/
|
|
27
|
+
export function renderMainJs(absPkgDir, templateDir = TEMPLATE_DIR) {
|
|
28
|
+
const mainTemplate = readFileSync(join(templateDir, 'main.js'), 'utf8')
|
|
29
|
+
return mainTemplate.split(STORIES_GLOB_TOKEN).join(detectStoriesGlob(absPkgDir))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Вміст канонічного `.storybook/preview.js` — verbatim з template (не залежить від пакета).
|
|
34
|
+
* Експортовано — переюз у `adopt/main.mjs`.
|
|
35
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
36
|
+
* @returns {string} вміст `preview.js`
|
|
37
|
+
*/
|
|
38
|
+
export function renderPreviewJs(templateDir = TEMPLATE_DIR) {
|
|
39
|
+
return readFileSync(join(templateDir, 'preview.js'), 'utf8')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Вміст канонічного `.storybook/mocks/gql-sse.js` — verbatim з template. Експортовано —
|
|
44
|
+
* переюз у `adopt/main.mjs`.
|
|
45
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
46
|
+
* @returns {string} вміст `mocks/gql-sse.js`
|
|
47
|
+
*/
|
|
48
|
+
export function renderMocksGqlSse(templateDir = TEMPLATE_DIR) {
|
|
49
|
+
return readFileSync(join(templateDir, 'mocks/gql-sse.js'), 'utf8')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Записує файл, створюючи батьківські каталоги й реєструючи запис для rollback.
|
|
54
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').FixContext} ctx fix-контекст рунга
|
|
55
|
+
* @param {string} absPath абсолютний шлях цільового файлу
|
|
56
|
+
* @param {string} content вміст файлу
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
function writeScaffoldFile(ctx, absPath, content) {
|
|
60
|
+
mkdirSync(dirname(absPath), { recursive: true })
|
|
61
|
+
ctx.recordWrite?.(absPath)
|
|
62
|
+
writeFileSync(absPath, content, 'utf8')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Резолвить абсолютний корінь пакета з `rootDir` violation.data (`.` — корінь репозиторію).
|
|
67
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
68
|
+
* @param {string} rootDir posix-relative корінь пакета
|
|
69
|
+
* @returns {string} абсолютний шлях кореня пакета
|
|
70
|
+
*/
|
|
71
|
+
function resolvePkgDir(cwd, rootDir) {
|
|
72
|
+
return rootDir === '.' ? cwd : join(cwd, rootDir)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
76
|
+
export const patterns = [
|
|
77
|
+
{
|
|
78
|
+
id: 'storybook-scaffold-main-js',
|
|
79
|
+
test: violations => violations.some(v => v.reason === 'missing-main-js'),
|
|
80
|
+
apply: (violations, ctx) => {
|
|
81
|
+
const targets = violations.filter(v => v.reason === 'missing-main-js' && typeof v.data?.rootDir === 'string')
|
|
82
|
+
if (targets.length === 0 || !ctx.concernDir) return { touchedFiles: [] }
|
|
83
|
+
|
|
84
|
+
const templateDir = join(ctx.concernDir, 'template')
|
|
85
|
+
const mocksTemplate = renderMocksGqlSse(templateDir)
|
|
86
|
+
|
|
87
|
+
const touchedFiles = []
|
|
88
|
+
for (const v of targets) {
|
|
89
|
+
const absPkgDir = resolvePkgDir(ctx.cwd, v.data.rootDir)
|
|
90
|
+
const rendered = renderMainJs(absPkgDir, templateDir)
|
|
91
|
+
|
|
92
|
+
const mainAbs = join(absPkgDir, '.storybook/main.js')
|
|
93
|
+
writeScaffoldFile(ctx, mainAbs, rendered)
|
|
94
|
+
touchedFiles.push(mainAbs)
|
|
95
|
+
|
|
96
|
+
const mocksAbs = join(absPkgDir, '.storybook/mocks/gql-sse.js')
|
|
97
|
+
if (!existsSync(mocksAbs)) {
|
|
98
|
+
writeScaffoldFile(ctx, mocksAbs, mocksTemplate)
|
|
99
|
+
touchedFiles.push(mocksAbs)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { touchedFiles, message: `.storybook/main.js: створено для ${targets.length} пакет(ів)` }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'storybook-scaffold-preview-js',
|
|
107
|
+
test: violations => violations.some(v => v.reason === 'missing-preview-js'),
|
|
108
|
+
apply: (violations, ctx) => {
|
|
109
|
+
const targets = violations.filter(v => v.reason === 'missing-preview-js' && typeof v.data?.rootDir === 'string')
|
|
110
|
+
if (targets.length === 0 || !ctx.concernDir) return { touchedFiles: [] }
|
|
111
|
+
|
|
112
|
+
const previewTemplate = renderPreviewJs(join(ctx.concernDir, 'template'))
|
|
113
|
+
|
|
114
|
+
const touchedFiles = []
|
|
115
|
+
for (const v of targets) {
|
|
116
|
+
const absPkgDir = resolvePkgDir(ctx.cwd, v.data.rootDir)
|
|
117
|
+
const previewAbs = join(absPkgDir, '.storybook/preview.js')
|
|
118
|
+
writeScaffoldFile(ctx, previewAbs, previewTemplate)
|
|
119
|
+
touchedFiles.push(previewAbs)
|
|
120
|
+
}
|
|
121
|
+
return { touchedFiles, message: `.storybook/preview.js: створено для ${targets.length} пакет(ів)` }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'storybook-scaffold-package-script',
|
|
126
|
+
test: violations => violations.some(v => v.reason === 'missing-storybook-script'),
|
|
127
|
+
apply: (violations, ctx) => {
|
|
128
|
+
const targets = violations.filter(v => v.reason === 'missing-storybook-script' && v.file)
|
|
129
|
+
const touchedFiles = []
|
|
130
|
+
for (const v of targets) {
|
|
131
|
+
const abs = join(ctx.cwd, v.file)
|
|
132
|
+
let pkg
|
|
133
|
+
try {
|
|
134
|
+
pkg = JSON.parse(readFileSync(abs, 'utf8'))
|
|
135
|
+
} catch {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
139
|
+
if (pkg.scripts.storybook === STORYBOOK_SCRIPT) continue
|
|
140
|
+
pkg.scripts.storybook = STORYBOOK_SCRIPT
|
|
141
|
+
ctx.recordWrite?.(abs)
|
|
142
|
+
writeFileSync(abs, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
143
|
+
touchedFiles.push(abs)
|
|
144
|
+
}
|
|
145
|
+
return touchedFiles.length > 0
|
|
146
|
+
? { touchedFiles, message: `scripts.storybook: встановлено у ${touchedFiles.length} package.json` }
|
|
147
|
+
: { touchedFiles: [] }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** @see ./docs/main.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
7
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
8
|
+
|
|
9
|
+
/** Канонічне значення `package.json#scripts.storybook` (storybook.mdc). */
|
|
10
|
+
export const STORYBOOK_SCRIPT = 'storybook dev -p 6006 --no-open'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Маркери канону `.storybook/main.js`, перевірені текстовим пошуком (без AST — рядки стабільні).
|
|
14
|
+
* Експортовано — той самий список переюзає `adopt/main.mjs` для diff-діагностики (не дублювати).
|
|
15
|
+
*/
|
|
16
|
+
export const MAIN_JS_MARKERS = [
|
|
17
|
+
{ token: '@storybook/vue3-vite', hint: 'framework @storybook/vue3-vite' },
|
|
18
|
+
{ token: 'viteFinal', hint: 'viteFinal-override vite.config пакета' },
|
|
19
|
+
{ token: "'vite-plugin-pages'", hint: 'фільтр vite-plugin-pages у viteFinal' },
|
|
20
|
+
{ token: "'vite-plugin-vue-layouts'", hint: 'фільтр vite-plugin-vue-layouts у viteFinal' }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
/** Маркери канону `.storybook/preview.js`. Експортовано — переюз у `adopt/main.mjs`. */
|
|
24
|
+
export const PREVIEW_JS_MARKERS = [
|
|
25
|
+
{ token: 'Quasar', hint: 'повний install Quasar' },
|
|
26
|
+
{ token: 'iconSet', hint: 'iconSet' },
|
|
27
|
+
{ token: 'iconMapFn', hint: 'iconMapFn (без нього внутрішні Quasar-іконки недоступні)' },
|
|
28
|
+
{ token: 'msw-storybook-addon', hint: 'msw-storybook-addon' },
|
|
29
|
+
{ token: 'onUnhandledRequest', hint: 'onUnhandledRequest-фільтр' }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Layout-детекція для stories-glob (ADR Кластер 2): `src/components/` присутній → glob
|
|
34
|
+
* звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по `src/`.
|
|
35
|
+
* Шлях відносний до `.storybook/` (де лежить сам `main.js`), тому з префіксом `../`.
|
|
36
|
+
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
37
|
+
* @returns {string} glob для `stories` у `.storybook/main.js`
|
|
38
|
+
*/
|
|
39
|
+
export function detectStoriesGlob(absPkgDir) {
|
|
40
|
+
return existsSync(join(absPkgDir, 'src/components'))
|
|
41
|
+
? '../src/components/**/*.stories.@(js|ts)'
|
|
42
|
+
: '../src/**/*.stories.@(js|ts)'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} content вміст файлу
|
|
47
|
+
* @param {{ token: string, hint: string }[]} markers очікувані канонічні маркери
|
|
48
|
+
* @returns {{ token: string, hint: string }[]} маркери, яких бракує
|
|
49
|
+
*/
|
|
50
|
+
export function missingMarkers(content, markers) {
|
|
51
|
+
return markers.filter(m => !content.includes(m.token))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Перевіряє один канонічний файл скафолду (`.storybook/main.js` або `.storybook/preview.js`):
|
|
56
|
+
* відсутність → `missingReason`-порушення з посиланням на `npx \@7n/rules fix storybook`;
|
|
57
|
+
* присутність без якогось канонічного маркера → `markerReason`-порушення на маркер.
|
|
58
|
+
* @param {string} absDir абсолютний корінь пакета
|
|
59
|
+
* @param {string} relFile posix-relative шлях файлу від кореня пакета (`.storybook/main.js`)
|
|
60
|
+
* @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
|
|
61
|
+
* @param {string} missingReason reason для порушення "файл відсутній"
|
|
62
|
+
* @param {string} markerReason reason для порушення "маркер відсутній"
|
|
63
|
+
* @param {string} label людський підпис пакета для повідомлень
|
|
64
|
+
* @param {string} rootDir root dir пакета (для violation.data)
|
|
65
|
+
* @param {string} fileRel posix-relative шлях файлу від кореня репозиторію (для violation.file)
|
|
66
|
+
* @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
async function checkCanonFile(
|
|
70
|
+
absDir,
|
|
71
|
+
relFile,
|
|
72
|
+
markers,
|
|
73
|
+
missingReason,
|
|
74
|
+
markerReason,
|
|
75
|
+
label,
|
|
76
|
+
rootDir,
|
|
77
|
+
fileRel,
|
|
78
|
+
reporter
|
|
79
|
+
) {
|
|
80
|
+
const abs = join(absDir, relFile)
|
|
81
|
+
if (existsSync(abs)) {
|
|
82
|
+
const content = await readFile(abs, 'utf8')
|
|
83
|
+
for (const m of missingMarkers(content, markers)) {
|
|
84
|
+
reporter.fail(`[${label}] ${relFile} не відповідає канону — бракує: ${m.hint} (storybook.mdc)`, {
|
|
85
|
+
reason: markerReason,
|
|
86
|
+
file: fileRel
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
reporter.fail(`[${label}] відсутній ${relFile} — канонічний скафолд: npx @7n/rules fix storybook (storybook.mdc)`, {
|
|
92
|
+
reason: missingReason,
|
|
93
|
+
file: fileRel,
|
|
94
|
+
data: { rootDir }
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Перевіряє один в-скоупі пакет: `.storybook/main.js`, `.storybook/preview.js`,
|
|
100
|
+
* `package.json#scripts.storybook`.
|
|
101
|
+
* @param {import('../scope/main.mjs').InScopePackage} pkgEntry пакет у скоупі
|
|
102
|
+
* @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
async function checkPackageScaffold({ rootDir, absDir, pkg }, reporter) {
|
|
106
|
+
const label = rootDir === '.' ? 'корінь' : rootDir
|
|
107
|
+
const relPrefix = rootDir === '.' ? '' : `${rootDir}/`
|
|
108
|
+
|
|
109
|
+
await checkCanonFile(
|
|
110
|
+
absDir,
|
|
111
|
+
'.storybook/main.js',
|
|
112
|
+
MAIN_JS_MARKERS,
|
|
113
|
+
'missing-main-js',
|
|
114
|
+
'main-js-marker-missing',
|
|
115
|
+
label,
|
|
116
|
+
rootDir,
|
|
117
|
+
`${relPrefix}.storybook/main.js`,
|
|
118
|
+
reporter
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await checkCanonFile(
|
|
122
|
+
absDir,
|
|
123
|
+
'.storybook/preview.js',
|
|
124
|
+
PREVIEW_JS_MARKERS,
|
|
125
|
+
'missing-preview-js',
|
|
126
|
+
'preview-js-marker-missing',
|
|
127
|
+
label,
|
|
128
|
+
rootDir,
|
|
129
|
+
`${relPrefix}.storybook/preview.js`,
|
|
130
|
+
reporter
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const scriptValue = pkg?.scripts?.storybook
|
|
134
|
+
if (scriptValue !== STORYBOOK_SCRIPT) {
|
|
135
|
+
const pkgJsonRel = `${relPrefix}package.json`
|
|
136
|
+
const current = scriptValue ? `'${scriptValue}'` : 'відсутній'
|
|
137
|
+
reporter.fail(
|
|
138
|
+
`[${label}] package.json#scripts.storybook має бути '${STORYBOOK_SCRIPT}' (зараз: ${current}) — storybook.mdc`,
|
|
139
|
+
{ reason: 'missing-storybook-script', file: pkgJsonRel, data: { rootDir } }
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Перевіряє канонічний Storybook-скафолд (`.storybook/main.js`, `.storybook/preview.js`,
|
|
146
|
+
* `package.json#scripts.storybook`) для всіх пакетів у скоупі (`scope/main.mjs`).
|
|
147
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
|
|
148
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
|
|
149
|
+
*/
|
|
150
|
+
export async function lint(ctx) {
|
|
151
|
+
const reporter = createViolationReporter(ctx)
|
|
152
|
+
const pkgs = await collectInScopeVuePackages(ctx.cwd)
|
|
153
|
+
|
|
154
|
+
if (pkgs.length === 0) {
|
|
155
|
+
reporter.pass('storybook: немає Vue component library пакетів у скоупі (storybook.mdc)')
|
|
156
|
+
return reporter.result()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const entry of pkgs) {
|
|
160
|
+
await checkPackageScaffold(entry, reporter)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return reporter.result()
|
|
164
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/scaffold/template
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------ | --------- |
|
|
9
|
+
| [main.js](main.md) | JS Module |
|
|
10
|
+
| [preview.js](preview.md) | JS Module |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/main.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5b21e6e2
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл задає канонічний Storybook-конфіг для Vue-компонентних бібліотек, які дотримуються правила `storybook` і ADR `канон-storybook-для-vue-компонентних-бібліотек`, щоб спільний формат прев’ю був відтворюваним у всіх таких пакетах. Він фіксує налаштування, потрібні для Storybook-перегляду Vue-компонентів, і зберігає лише ті ресурси та плагіни, без яких прев’ю не працює як очікується.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. Формує канонічний Storybook-конфіг для Vue-компонентної бібліотеки, щоб ізольований перегляд компонентів працював однаково в усіх пакетах, які дотримуються цього правила.
|
|
21
|
+
2. Підтягує базові налаштування застосунку-споживача й зливає їх із Storybook-налаштуваннями, щоб зберегти спільні параметри рендерингу, шляхів і CSS.
|
|
22
|
+
3. Вилучає плагіни, які залежать від повної маршрутизації або layout-інфраструктури застосунку, бо в Storybook вони не дають користі для одного компонента.
|
|
23
|
+
4. Замінює Vue- і Quasar-інтеграцію на власні екземпляри у фіксованому порядку, щоб шаблони компонентів і Quasar-стилі оброблялися передбачувано.
|
|
24
|
+
5. Залишає інші корисні плагіни пакета, щоб у Storybook зберігалися допоміжні можливості на кшталт auto-import чи SVG-підтримки.
|
|
25
|
+
6. Підключає публічні assets із теки `public`, щоб preview міг ініціалізувати service worker для `msw`.
|
|
26
|
+
7. Якщо канонічний файл пошкоджено або видалено, відтворюється правилом `storybook` через `npx @7n/rules fix storybook`.
|
|
27
|
+
|
|
28
|
+
## Гарантії поведінки
|
|
29
|
+
|
|
30
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: preview.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/preview.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5590ef55
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл задає канонічний preview для Vue-компонентних бібліотек відповідно до `storybook.mdc` та ADR `канон-storybook-для-vue-компонентних-бібліотек`. Він існує, щоб Storybook мав відтворювану базову конфігурацію для перегляду компонентів у бібліотеках.
|
|
17
|
+
|
|
18
|
+
Файл згенеровано правилом `storybook`: команда `npx @7n/rules fix storybook` відтворює його, якщо файл видалено або зламано канон.
|
|
19
|
+
|
|
20
|
+
Файл є read-only: не пише у ФС чи БД.
|
|
21
|
+
|
|
22
|
+
## Поведінка
|
|
23
|
+
|
|
24
|
+
1. Готує Storybook як канонічне середовище для Vue-компонентних бібліотек, щоб stories працювали наближено до реального застосунку-споживача.
|
|
25
|
+
|
|
26
|
+
2. Підключає базові стилі Quasar, щоб компоненти відображалися з очікуваним виглядом у Storybook.
|
|
27
|
+
|
|
28
|
+
3. Вмикає перехоплення мережевих запитів у Storybook, щоб компоненти могли працювати з контрольованими mock-відповідями.
|
|
29
|
+
|
|
30
|
+
4. Ігнорує очікувані same-origin GET-запити dev-сервера, зокрема для HMR і статичних файлів, щоб вони не створювали зайвого шуму.
|
|
31
|
+
|
|
32
|
+
5. Позначає інші мережеві запити, які не підпадають під тихий пропуск, попередженням, а не помилкою збірки, щоб дати м’який сигнал без блокування роботи Storybook.
|
|
33
|
+
|
|
34
|
+
6. Встановлює Quasar у Storybook повністю, щоб бібліотечні компоненти мали доступ до глобальних Quasar-примітивів так само, як у реальному застосунку.
|
|
35
|
+
|
|
36
|
+
7. Додає Quasar-плагіни для повідомлень і діалогів, щоб stories могли відтворювати сценарії з користувацькими сповіщеннями та модальними взаємодіями.
|
|
37
|
+
|
|
38
|
+
8. Налаштовує набір іконок Quasar, щоб компоненти коректно показували матеріальні SVG-іконки поза повним Quasar CLI-середовищем.
|
|
39
|
+
|
|
40
|
+
9. Передає Storybook декоратор для mock service worker, щоб мережеве оточення застосовувалося до stories централізовано.
|
|
41
|
+
|
|
42
|
+
10. Налаштовує автоматичне розпізнавання controls-панелі Storybook для кольорів і дат, щоб інтерфейс керування props був зручнішим для перегляду компонентів.
|
|
43
|
+
|
|
44
|
+
## Гарантії поведінки
|
|
45
|
+
|
|
46
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний конфіг Storybook для Vue-компонентних бібліотек (storybook.mdc, ADR
|
|
3
|
+
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
|
+
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfigFromFile, mergeConfig } from 'vite'
|
|
7
|
+
import vue from '@vitejs/plugin-vue'
|
|
8
|
+
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
|
9
|
+
|
|
10
|
+
// Плагіни власного Vite-збирача Storybook — порядок ФІКСОВАНИЙ: @vitejs/plugin-vue
|
|
11
|
+
// ПЕРЕД quasar() (інакше Quasar-плагін не бачить SFC, уже скомпільований plugin-vue).
|
|
12
|
+
const OWN_PLUGINS = [vue({ template: { transformAssetUrls } }), quasar({ sassVariables: true })]
|
|
13
|
+
|
|
14
|
+
// Плагіни файлової маршрутизації додатка-споживача — не мають сенсу в ізольованому
|
|
15
|
+
// рендері одного компонента, знімаються при обʼєднанні з vite.config пакета.
|
|
16
|
+
const INCOMPATIBLE_PLUGIN_NAMES = new Set(['vite-plugin-pages', 'vite-plugin-vue-layouts'])
|
|
17
|
+
|
|
18
|
+
/** @type {import('@storybook/vue3-vite').StorybookConfig} */
|
|
19
|
+
const config = {
|
|
20
|
+
stories: ['__STORYBOOK_STORIES_GLOB__'],
|
|
21
|
+
framework: {
|
|
22
|
+
name: '@storybook/vue3-vite',
|
|
23
|
+
options: {}
|
|
24
|
+
},
|
|
25
|
+
// Публічний asset для msw service worker, який ініціалізує preview.js.
|
|
26
|
+
staticDirs: ['./public'],
|
|
27
|
+
async viteFinal(storybookConfig) {
|
|
28
|
+
const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, undefined, process.cwd())
|
|
29
|
+
const userPlugins = (loaded?.config?.plugins ?? [])
|
|
30
|
+
.flat()
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.filter(p => !INCOMPATIBLE_PLUGIN_NAMES.has(p.name))
|
|
33
|
+
// vue()/quasar() пакета замінюємо власними екземплярами у фіксованому порядку вище —
|
|
34
|
+
// не дублюємо; решта плагінів пакета (auto-import, svg-loader тощо) лишається.
|
|
35
|
+
.filter(p => p.name !== 'vite:vue' && p.name !== 'quasar')
|
|
36
|
+
|
|
37
|
+
return mergeConfig(storybookConfig, {
|
|
38
|
+
resolve: loaded?.config?.resolve,
|
|
39
|
+
css: loaded?.config?.css,
|
|
40
|
+
plugins: [...OWN_PLUGINS, ...userPlugins]
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default config
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: gql-sse.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/mocks/gql-sse.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: fe63fba2
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Надає єдиний MSW-хелпер `sseSubscription` для мокання Apollo GraphQL-підписок у Storybook через `graphql-sse`. Кожна подія виконання підписки надсилається як окремий SSE `next`-запис у distinct-connection режимі: `event: next\ndata: <JSON>\n\n`, щоб пакети не дублювали формат цього wire-протоколу.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. `sseSubscription` приймає послідовність готових GraphQL execution-result повідомлень для однієї підписки.
|
|
21
|
+
|
|
22
|
+
2. Для кожного повідомлення створює окрему SSE-подію типу `next`, сумісну з wire-протоколом `graphql-sse` у distinct-connection режимі.
|
|
23
|
+
|
|
24
|
+
3. Повертає потік тіла відповіді, який MSW-хендлер може використати для мокання Apollo GraphQL-підписки у Storybook.
|
|
25
|
+
|
|
26
|
+
4. Завершує потік після надсилання всіх повідомлень, щоб сценарій підписки мав детермінований кінець.
|
|
27
|
+
|
|
28
|
+
5. Централізує формат тестових subscription-подій, щоб пакети не дублювали власну реалізацію цього протоколу.
|
|
29
|
+
|
|
30
|
+
## Публічний API
|
|
31
|
+
|
|
32
|
+
- sseSubscription — Формує SSE-стрім тіла відповіді для MSW-хендлера підписки graphql-sse.
|
|
33
|
+
|
|
34
|
+
## Гарантії поведінки
|
|
35
|
+
|
|
36
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/scaffold/template/mocks
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/mocks/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------ | --------- |
|
|
9
|
+
| [gql-sse.js](gql-sse.md) | JS Module |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний MSW-хелпер для мокання Apollo GraphQL-підписок через `graphql-sse` (storybook.mdc,
|
|
3
|
+
* ADR канон-storybook-для-vue-компонентних-бібліотек, Кластер 3). Wire-протокол
|
|
4
|
+
* `graphql-sse` у distinct-connection режимі — кожна подія виконання підписки надсилається
|
|
5
|
+
* як окремий SSE `next`-запис: `event: next\ndata: <JSON>\n\n`. Одне джерело істини для цього
|
|
6
|
+
* протоколу — не переносити копію логіки в кожен пакет окремо.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Формує SSE-стрім тіла відповіді для MSW-хендлера підписки graphql-sse.
|
|
11
|
+
* @param {Array<object>} payloads послідовність GraphQL execution-result payload-ів — кожен
|
|
12
|
+
* елемент те, що `graphql-sse` надіслав би як окрему `next`-подію одного виконання підписки
|
|
13
|
+
* @returns {ReadableStream<Uint8Array>} SSE body-стрім для MSW-response (`HttpResponse` body)
|
|
14
|
+
*/
|
|
15
|
+
export function sseSubscription(payloads) {
|
|
16
|
+
const encoder = new TextEncoder()
|
|
17
|
+
return new ReadableStream({
|
|
18
|
+
start(controller) {
|
|
19
|
+
for (const payload of payloads) {
|
|
20
|
+
controller.enqueue(encoder.encode(`event: next\ndata: ${JSON.stringify(payload)}\n\n`))
|
|
21
|
+
}
|
|
22
|
+
controller.close()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний preview для Vue-компонентних бібліотек (storybook.mdc, ADR
|
|
3
|
+
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
|
+
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
|
+
*/
|
|
6
|
+
import { setup } from '@storybook/vue3'
|
|
7
|
+
import { Dialog, iconSet as quasarBuiltinIconSet, Notify, Quasar } from 'quasar'
|
|
8
|
+
import iconSet from 'quasar/icon-set/svg-material-icons'
|
|
9
|
+
import { initialize, mswDecorator } from 'msw-storybook-addon'
|
|
10
|
+
|
|
11
|
+
import 'quasar/dist/quasar.css'
|
|
12
|
+
|
|
13
|
+
// msw-storybook-addon: перехоплює мережеві запити компонентів у Storybook. Same-origin GET
|
|
14
|
+
// (Vite HMR, статичні файли dev-сервера) — очікуваний шум, мовчки пропускаємо; усе інше —
|
|
15
|
+
// warn (не білд-помилка навмисно, хвиля 1 — м'який сигнал, не гейт).
|
|
16
|
+
initialize({
|
|
17
|
+
onUnhandledRequest(request, print) {
|
|
18
|
+
const url = new URL(request.url)
|
|
19
|
+
if (request.method === 'GET' && url.origin === globalThis.location?.origin) return
|
|
20
|
+
print.warning()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
setup(app => {
|
|
25
|
+
// Повний install Quasar (не окремі компоненти) — бібліотека компонентів очікує
|
|
26
|
+
// глобально зареєстровані Quasar-примітиви (QBtn, QCard тощо) так само, як у реальному
|
|
27
|
+
// додатку-споживачі.
|
|
28
|
+
app.use(Quasar, {
|
|
29
|
+
plugins: { Notify, Dialog },
|
|
30
|
+
iconSet,
|
|
31
|
+
// iconMapFn — без нього внутрішні Quasar-компоненти (напр. стрілка QSelect) не
|
|
32
|
+
// резолвлять власні вбудовані іконки поза full Quasar CLI build.
|
|
33
|
+
iconMapFn(iconName) {
|
|
34
|
+
return quasarBuiltinIconSet.iconMapFn?.(iconName)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** @type {import('@storybook/vue3').Preview} */
|
|
40
|
+
const preview = {
|
|
41
|
+
decorators: [mswDecorator],
|
|
42
|
+
parameters: {
|
|
43
|
+
controls: { matchers: { color: /(background|color)$/iu, date: /Date$/u } }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default preview
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scope/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 04c71162
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 90
|
|
10
|
+
issues: internal-name:isVueComponentLibraryPkg,judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл визначає, які workspace-пакети мають потрапити в скоуп Storybook: спирається на `.n-rules.json`, `.n-cursor.json` і `package.json`, враховує opt-out, поріг Vue-файлів, стандартний build та окремий прапорець для app-проєктів. Також він fail-safe перевіряє, що `storybook.optOut` не містить посилань на неіснуючі пакети.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `VUE_FILE_THRESHOLD` задає мінімальну кількість Vue-файлів, після якої пакет може потрапити в скоуп Storybook.
|
|
21
|
+
- `readStorybookOptOut` читає з `.n-rules.json` або legacy `.n-cursor.json` перелік workspace-пакетів, які свідомо виключені зі скоупу Storybook; за відсутності або пошкодженості конфіга повертає порожній перелік.
|
|
22
|
+
- `readDetectAppsFlag` читає з `.n-rules.json` або legacy `.n-cursor.json` явний прапорець включення app-проєктів у детекцію Storybook; за замовчуванням лишає їх поза скоупом.
|
|
23
|
+
- `countVueFiles` рахує Vue-файли в дереві пакета з урахуванням шляхів, виключених конфігурацією і правилами ігнорування.
|
|
24
|
+
- `hasStandardBuild` визначає, чи має пакет підтримуваний Vite build-конфіг у корені, потрібний для канонічного Storybook setup.
|
|
25
|
+
- `isVueAppPkg` визначає, чи виглядає `package.json` як Vue app-проєкт, а не компонентна бібліотека.
|
|
26
|
+
- `collectInScopeVuePackages` збирає workspace-пакети зі стандартним build, достатньою кількістю Vue-файлів і без `storybook.optOut`; app-проєкти додає лише після явного увімкнення відповідного прапорця.
|
|
27
|
+
- `lint` перевіряє гігієну `storybook.optOut`: кожен запис має посилатися на наявний workspace-пакет, інакше звітує про застаріле налаштування.
|
|
28
|
+
|
|
29
|
+
## Публічний API
|
|
30
|
+
|
|
31
|
+
- VUE_FILE_THRESHOLD — Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1).
|
|
32
|
+
- readStorybookOptOut — Читає `storybook.optOut` з `.n-rules.json` (fallback — legacy `.n-cursor.json`). Толерантно до
|
|
33
|
+
відсутнього файлу/поля/невалідного JSON — повертає порожній масив (open-by-default, як
|
|
34
|
+
`read-n-rules-config-lite.mjs`). Значення — root dir пакетів (`.` для кореня, `packages/ui` тощо),
|
|
35
|
+
той самий формат, що повертає `getMonorepoPackageRootDirs`.
|
|
36
|
+
- readDetectAppsFlag — Читає прапорець хвилі 2 `storybook.detectApps` з `.n-rules.json`. За замовчуванням `false` —
|
|
37
|
+
детекція app-проєктів (`vue` у dependencies + `src/pages/`) лишається відкритим питанням ADR
|
|
38
|
+
і не впливає на скоуп, доки консюмер-репо не увімкне прапорець явно.
|
|
39
|
+
- countVueFiles — Рахує `.vue`-файли в дереві пакета (поважає `.gitignore` й `ignore` з `.n-rules.json` через
|
|
40
|
+
`walkDir`/`ignorePaths` — той самий обхід, що й `vue/packages`).
|
|
41
|
+
- hasStandardBuild — Чи має пакет "стандартний" build — розпізнаваний `vite.config.{js,ts,mjs}` у корені пакета.
|
|
42
|
+
Канонічний `.storybook/main.js` спирається саме на цей файл (`viteFinal` мерджить його
|
|
43
|
+
плагіни) — без нього автоматичний скафолд неможливий, і пакет пропускається мовчки
|
|
44
|
+
(ADR Кластер 1: "skip пакетів із нестандартним build").
|
|
45
|
+
- isVueAppPkg — Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
|
|
46
|
+
`peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
|
|
47
|
+
модуль пізніше), але результат впливає на скоуп лише за прапорця `storybook.detectApps`.
|
|
48
|
+
- collectInScopeVuePackages — Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
|
|
49
|
+
(`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
|
|
50
|
+
з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
|
|
51
|
+
стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
|
|
52
|
+
прапорця `storybook.detectApps` у `.n-rules.json`.
|
|
53
|
+
- lint — Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
|
|
54
|
+
неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
|
|
55
|
+
opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
|
|
56
|
+
покриті тестами напряму; тут лише конфіг-гігієна.
|
|
57
|
+
|
|
58
|
+
## Гарантії поведінки
|
|
59
|
+
|
|
60
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
61
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
62
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|