@nitra/ci-docs 0.0.2 → 1.0.1
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 +29 -3
- package/package.json +7 -10
- package/src/cli.mjs +15 -13
- package/src/commit-push/main.mjs +100 -0
- package/src/sync-schema/main.mjs +39 -29
- package/types/cli.d.mts +2 -0
- package/types/commit-push/main.d.mts +45 -0
- package/types/sync-schema/main.d.mts +142 -0
- package/src/sync-schema/__fixtures__/new-schema.graphql +0 -10
- package/src/sync-schema/__fixtures__/old-schema.graphql +0 -9
- package/src/sync-schema/main.test.mjs +0 -280
- package/types/index.d.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [1.0.1] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `writeGithubOutput()` тепер бере `GITHUB_OUTPUT` через `env` з `node:process` (вимога `n-js-run`).
|
|
12
|
+
- JSDoc-описи для helper-функцій у тестах (`setupTmpDocs`, `setupBareDocs`, `startMockGraphql`) — повний набір `@param`/`@returns`.
|
|
13
|
+
- `cli.mjs` рефакторено з `process.exit()` на `process.exitCode` (вимога `n-no-process-exit`).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `tsconfig.emit-types.json` для генерації `.d.mts` без штучного `src/index.js` (Variant B з `n-npm-module`).
|
|
18
|
+
- Локальні регулярки у тестах винесено в module-scope константи (вимога `e18e/prefer-static-regex`).
|
|
19
|
+
|
|
20
|
+
## [1.0.0] - 2026-05-12
|
|
8
21
|
|
|
9
22
|
### Added
|
|
10
23
|
|
|
11
|
-
- CLI `ci-docs
|
|
12
|
-
-
|
|
24
|
+
- CLI `ci-docs sync-schema` — інтроспектить **будь-який** GraphQL-ендпоінт, рахує SemVer-bump через `graphql-inspector`, оновлює CHANGELOG і пише SDL у `npm/schema/`.
|
|
25
|
+
- `--endpoint <url>` (обовʼязковий) — GraphQL-ендпоінт.
|
|
26
|
+
- `--header "K: V"` (повторюваний) — будь-які HTTP-заголовки (Hasura admin secret, Bearer token тощо).
|
|
27
|
+
- `--docs <path>` (default `./docs`), `--schema-name <file>` (default `maya.graphql`), `--source-ref <text>` (default `unknown`).
|
|
28
|
+
- CLI `ci-docs commit-push` — git add/commit/push для перелічених файлів.
|
|
29
|
+
- `--repo <path>`, `--message <msg>`, `--file <path>` (повторюваний), `--author-name`, `--author-email` — обовʼязкові.
|
|
30
|
+
- `--branch <name>` (default `main`), `--remote <name>` (default `origin`) — опціональні.
|
|
31
|
+
- Якщо staged-зміни відсутні, ні коміту, ні push не буде.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- (BREAKING) Перейменовано CLI-аргументи `sync-schema`: `--hasura-url` → `--endpoint`, `--hasura-secret` → `--header`, `--db-sha` → `--source-ref`.
|
|
36
|
+
- (BREAKING) Текст CHANGELOG, що генерується, більше не згадує Hasura: "Оновлено GraphQL-схему (`<ref>`)." та "Початкове додавання GraphQL-схеми.".
|
|
37
|
+
- (BREAKING) `main()` та `formatChangelogBlock()` приймають `sourceRef` замість `dbSha`. `fetchSdl(endpoint, headers)` — headers як обʼєкт замість окремого `adminSecret`.
|
|
38
|
+
- Видалено CLI-аргумент `--new-schema` зі `sync-schema` (живий ендпоінт — єдина точка входу).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/ci-docs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Nitra CI tooling: sync GraphQL schema to a docs npm package (and more to come).",
|
|
5
5
|
"homepage": "https://github.com/nitra/ci-docs",
|
|
6
6
|
"bugs": {
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "git+https://github.com/nitra/ci-docs.git"
|
|
14
14
|
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"ci-docs": "./src/cli.mjs"
|
|
17
|
+
},
|
|
15
18
|
"files": [
|
|
16
19
|
"src",
|
|
17
20
|
"types",
|
|
18
21
|
"CHANGELOG.md"
|
|
19
22
|
],
|
|
20
23
|
"type": "module",
|
|
21
|
-
"types": "./types/
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
+
"types": "./types/cli.d.mts",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"@graphql-inspector/core": "^6.2.1",
|
|
@@ -29,11 +32,5 @@
|
|
|
29
32
|
"engines": {
|
|
30
33
|
"bun": ">=1.3",
|
|
31
34
|
"node": ">=24"
|
|
32
|
-
},
|
|
33
|
-
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"@nitra/cursor": "^1.9.4"
|
|
38
35
|
}
|
|
39
36
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* Запуск: `npx -y @nitra/ci-docs <subcommand> [args...]`
|
|
6
6
|
*
|
|
7
7
|
* Доступні subcommands:
|
|
8
|
-
* sync-schema — інтроспект
|
|
8
|
+
* sync-schema — інтроспект GraphQL-ендпоінта → bump → CHANGELOG → запис SDL
|
|
9
|
+
* commit-push — git add/commit/push для оновлених файлів
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const SUBCOMMANDS = {
|
|
12
|
-
'sync-schema': () => import('./sync-schema/main.mjs').then(m => m.cli)
|
|
13
|
+
'sync-schema': () => import('./sync-schema/main.mjs').then(m => m.cli),
|
|
14
|
+
'commit-push': () => import('./commit-push/main.mjs').then(m => m.cli)
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const [subcommand, ...rest] = process.argv.slice(2)
|
|
@@ -21,15 +23,15 @@ Subcommands:
|
|
|
21
23
|
${Object.keys(SUBCOMMANDS).join('\n ')}
|
|
22
24
|
|
|
23
25
|
Run \`ci-docs <subcommand> --help\` for subcommand-specific options.`)
|
|
24
|
-
process.
|
|
26
|
+
process.exitCode = subcommand ? 0 : 2
|
|
27
|
+
} else {
|
|
28
|
+
const loader = SUBCOMMANDS[subcommand]
|
|
29
|
+
if (loader) {
|
|
30
|
+
const run = await loader()
|
|
31
|
+
await run(rest)
|
|
32
|
+
} else {
|
|
33
|
+
console.error(`Unknown subcommand: ${subcommand}`)
|
|
34
|
+
console.error(`Available: ${Object.keys(SUBCOMMANDS).join(', ')}`)
|
|
35
|
+
process.exitCode = 2
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
|
-
|
|
27
|
-
const loader = SUBCOMMANDS[subcommand]
|
|
28
|
-
if (!loader) {
|
|
29
|
-
console.error(`Unknown subcommand: ${subcommand}`)
|
|
30
|
-
console.error(`Available: ${Object.keys(SUBCOMMANDS).join(', ')}`)
|
|
31
|
-
process.exit(2)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const run = await loader()
|
|
35
|
-
await run(rest)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Викликає `git` як зовнішній процес з фіксованим CWD.
|
|
6
|
+
* @param {string} cwd робоча директорія
|
|
7
|
+
* @param {string[]} args аргументи `git`
|
|
8
|
+
* @returns {string} stdout (UTF-8, без trailing newline)
|
|
9
|
+
*/
|
|
10
|
+
function git(cwd, args) {
|
|
11
|
+
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'inherit'], encoding: 'utf8' }).trimEnd()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Перевіряє чи є хоч якісь стейджовані зміни.
|
|
16
|
+
* @param {string} repo шлях до робочого дерева git-репо
|
|
17
|
+
* @returns {boolean} true якщо є staged-зміни (diff --cached не порожній)
|
|
18
|
+
*/
|
|
19
|
+
function hasStagedChanges(repo) {
|
|
20
|
+
try {
|
|
21
|
+
execFileSync('git', ['diff', '--cached', '--quiet'], { cwd: repo, stdio: 'ignore' })
|
|
22
|
+
return false
|
|
23
|
+
} catch {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Стейджить файли, робить commit + push. Якщо файли не несуть змін — нічого не комітить і не пушить.
|
|
30
|
+
* @param {{repo: string, files: string[], message: string, authorName: string, authorEmail: string, branch?: string, remote?: string}} params параметри
|
|
31
|
+
* @returns {{committed: boolean, sha: string|null}} результат: чи був коміт і його SHA
|
|
32
|
+
*/
|
|
33
|
+
export function main({ repo, files, message, authorName, authorEmail, branch = 'main', remote = 'origin' }) {
|
|
34
|
+
if (!files.length) throw new Error('At least one --file is required')
|
|
35
|
+
|
|
36
|
+
git(repo, ['config', 'user.name', authorName])
|
|
37
|
+
git(repo, ['config', 'user.email', authorEmail])
|
|
38
|
+
|
|
39
|
+
git(repo, ['add', '--', ...files])
|
|
40
|
+
|
|
41
|
+
if (!hasStagedChanges(repo)) {
|
|
42
|
+
return { committed: false, sha: null }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
git(repo, ['commit', '-m', message])
|
|
46
|
+
const sha = git(repo, ['rev-parse', 'HEAD'])
|
|
47
|
+
git(repo, ['push', remote, `HEAD:${branch}`])
|
|
48
|
+
|
|
49
|
+
return { committed: true, sha }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* CLI-обгортка для commit-push. Параметри як `--key value`.
|
|
54
|
+
*
|
|
55
|
+
* Обовʼязкові:
|
|
56
|
+
* --repo <path> шлях до git-репо
|
|
57
|
+
* --message <msg> повідомлення коміту
|
|
58
|
+
* --file <path> повторюваний; шлях файлу від кореня репо (мінімум один)
|
|
59
|
+
* --author-name <name> Git user.name
|
|
60
|
+
* --author-email <email> Git user.email
|
|
61
|
+
*
|
|
62
|
+
* Необовʼязкові:
|
|
63
|
+
* --branch <name> цільова гілка (default 'main')
|
|
64
|
+
* --remote <name> remote (default 'origin')
|
|
65
|
+
* @param {string[]} [argv] аргументи. Default — process.argv.slice(2).
|
|
66
|
+
* @returns {{committed: boolean, sha: string|null}} результат main()
|
|
67
|
+
*/
|
|
68
|
+
export function cli(argv = process.argv.slice(2)) {
|
|
69
|
+
const { values } = parseArgs({
|
|
70
|
+
args: argv,
|
|
71
|
+
options: {
|
|
72
|
+
repo: { type: 'string' },
|
|
73
|
+
message: { type: 'string' },
|
|
74
|
+
file: { type: 'string', multiple: true, default: [] },
|
|
75
|
+
'author-name': { type: 'string' },
|
|
76
|
+
'author-email': { type: 'string' },
|
|
77
|
+
branch: { type: 'string', default: 'main' },
|
|
78
|
+
remote: { type: 'string', default: 'origin' }
|
|
79
|
+
},
|
|
80
|
+
allowPositionals: false,
|
|
81
|
+
strict: true
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
for (const key of ['repo', 'message', 'author-name', 'author-email']) {
|
|
85
|
+
if (!values[key]) throw new Error(`--${key} is required`)
|
|
86
|
+
}
|
|
87
|
+
if (values.file.length === 0) throw new Error('At least one --file is required')
|
|
88
|
+
|
|
89
|
+
const result = main({
|
|
90
|
+
repo: values.repo,
|
|
91
|
+
files: values.file,
|
|
92
|
+
message: values.message,
|
|
93
|
+
authorName: values['author-name'],
|
|
94
|
+
authorEmail: values['author-email'],
|
|
95
|
+
branch: values.branch,
|
|
96
|
+
remote: values.remote
|
|
97
|
+
})
|
|
98
|
+
console.log(JSON.stringify(result, null, 2))
|
|
99
|
+
return result
|
|
100
|
+
}
|
package/src/sync-schema/main.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
|
+
import { env } from 'node:process'
|
|
3
4
|
import { parseArgs } from 'node:util'
|
|
4
5
|
import { buildSchema, buildClientSchema, printSchema, getIntrospectionQuery } from 'graphql'
|
|
5
6
|
import { diff } from '@graphql-inspector/core'
|
|
@@ -49,17 +50,17 @@ export function classifyChanges(changes) {
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Будує markdown-блок changelog для версії.
|
|
52
|
-
* @param {{version: string, date: string,
|
|
53
|
+
* @param {{version: string, date: string, sourceRef: string, sections: {added: string[], removed: string[], changed: string[]}, first?: boolean}} params параметри блоку
|
|
53
54
|
* @returns {string} markdown-блок з трейлінговим порожнім рядком
|
|
54
55
|
*/
|
|
55
|
-
export function formatChangelogBlock({ version, date,
|
|
56
|
+
export function formatChangelogBlock({ version, date, sourceRef, sections, first = false }) {
|
|
56
57
|
const lines = [`## [${version}] - ${date}`, '']
|
|
57
58
|
|
|
58
59
|
const changed = [...sections.changed]
|
|
59
60
|
if (first) {
|
|
60
|
-
changed.unshift('Початкове додавання GraphQL
|
|
61
|
+
changed.unshift('Початкове додавання GraphQL-схеми.')
|
|
61
62
|
} else {
|
|
62
|
-
changed.unshift(`Оновлено GraphQL-схему
|
|
63
|
+
changed.unshift(`Оновлено GraphQL-схему (\`${sourceRef}\`).`)
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
if (sections.added.length > 0) {
|
|
@@ -178,25 +179,22 @@ export function bumpVersion(npmDir, kind) {
|
|
|
178
179
|
* @returns {void}
|
|
179
180
|
*/
|
|
180
181
|
export function writeGithubOutput(values) {
|
|
181
|
-
const outPath =
|
|
182
|
+
const outPath = env.GITHUB_OUTPUT
|
|
182
183
|
if (!outPath) return
|
|
183
184
|
const lines = Object.entries(values).map(([k, v]) => `${k}=${v}`)
|
|
184
185
|
appendFileSync(outPath, lines.join('\n') + '\n')
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
|
-
* Шле introspection-запит до
|
|
189
|
-
* @param {string}
|
|
190
|
-
* @param {string} [
|
|
189
|
+
* Шле introspection-запит до GraphQL-ендпоінта і повертає SDL-рядок.
|
|
190
|
+
* @param {string} endpoint URL GraphQL-ендпоінта
|
|
191
|
+
* @param {Record<string, string>} [headers] додаткові HTTP-заголовки (наприклад `{ 'X-Hasura-Admin-Secret': '...' }`)
|
|
191
192
|
* @returns {Promise<string>} SDL-схема у вигляді рядка
|
|
192
193
|
*/
|
|
193
|
-
export async function fetchSdl(
|
|
194
|
-
const res = await fetch(
|
|
194
|
+
export async function fetchSdl(endpoint, headers = {}) {
|
|
195
|
+
const res = await fetch(endpoint, {
|
|
195
196
|
method: 'POST',
|
|
196
|
-
headers: {
|
|
197
|
-
'Content-Type': 'application/json',
|
|
198
|
-
...(adminSecret ? { 'X-Hasura-Admin-Secret': adminSecret } : {})
|
|
199
|
-
},
|
|
197
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
200
198
|
body: JSON.stringify({ query: getIntrospectionQuery() })
|
|
201
199
|
})
|
|
202
200
|
if (!res.ok) throw new Error(`Introspection failed: ${res.status} ${res.statusText}`)
|
|
@@ -205,12 +203,23 @@ export async function fetchSdl(hasuraUrl, adminSecret) {
|
|
|
205
203
|
return printSchema(buildClientSchema(data))
|
|
206
204
|
}
|
|
207
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Парсить рядок `Key: Value` у пару `[key, value]`. Помилка, якщо нема `:`.
|
|
208
|
+
* @param {string} raw сирий header
|
|
209
|
+
* @returns {[string, string]} key, value (обрізані з обох боків)
|
|
210
|
+
*/
|
|
211
|
+
export function parseHeader(raw) {
|
|
212
|
+
const idx = raw.indexOf(':')
|
|
213
|
+
if (idx === -1) throw new Error(`Invalid --header value (expected "Key: Value"): ${raw}`)
|
|
214
|
+
return [raw.slice(0, idx).trim(), raw.slice(idx + 1).trim()]
|
|
215
|
+
}
|
|
216
|
+
|
|
208
217
|
/**
|
|
209
218
|
* Оркеструє весь flow: diff схем → bump → CHANGELOG → запис SDL.
|
|
210
|
-
* @param {{newSdl: string, docsRoot: string,
|
|
219
|
+
* @param {{newSdl: string, docsRoot: string, sourceRef: string, date: string, schemaFilename?: string}} params параметри запуску
|
|
211
220
|
* @returns {Promise<{changed: boolean, bump: 'minor'|'patch'|null, version: string|null}>} результат
|
|
212
221
|
*/
|
|
213
|
-
export async function main({ newSdl, docsRoot,
|
|
222
|
+
export async function main({ newSdl, docsRoot, sourceRef, date, schemaFilename = 'maya.graphql' }) {
|
|
214
223
|
const oldSchemaPath = `${docsRoot}/npm/schema/${schemaFilename}`
|
|
215
224
|
const oldSdl = readSdl(oldSchemaPath)
|
|
216
225
|
|
|
@@ -226,7 +235,7 @@ export async function main({ newSdl, docsRoot, dbSha, date, schemaFilename = 'ma
|
|
|
226
235
|
const npmDir = `${docsRoot}/npm`
|
|
227
236
|
const version = bumpVersion(npmDir, bump)
|
|
228
237
|
|
|
229
|
-
const block = formatChangelogBlock({ version, date,
|
|
238
|
+
const block = formatChangelogBlock({ version, date, sourceRef, sections, first })
|
|
230
239
|
const changelogPath = `${npmDir}/CHANGELOG.md`
|
|
231
240
|
const existingChangelog = readFileSync(changelogPath, 'utf8')
|
|
232
241
|
writeFileSync(changelogPath, prependChangelog(existingChangelog, block))
|
|
@@ -241,14 +250,14 @@ export async function main({ newSdl, docsRoot, dbSha, date, schemaFilename = 'ma
|
|
|
241
250
|
* CLI-обгортка для sync-schema. Приймає параметри як `--key value`.
|
|
242
251
|
*
|
|
243
252
|
* Обовʼязковий:
|
|
244
|
-
* --
|
|
253
|
+
* --endpoint <url> URL GraphQL-ендпоінта для introspection
|
|
245
254
|
*
|
|
246
255
|
* Необовʼязкові:
|
|
247
|
-
* --
|
|
256
|
+
* --header "K: V" HTTP-заголовок (повторюваний; наприклад: `--header "X-Hasura-Admin-Secret: ..."`,
|
|
257
|
+
* `--header "Authorization: Bearer ..."`)
|
|
248
258
|
* --docs <path> корінь docs-репо (default './docs')
|
|
249
259
|
* --schema-name <file> назва файлу в `npm/schema/` (default 'maya.graphql')
|
|
250
|
-
* --
|
|
251
|
-
*
|
|
260
|
+
* --source-ref <ref> текст, що йде у CHANGELOG як посилання на джерело (default 'unknown')
|
|
252
261
|
* @param {string[]} [argv] аргументи (без 'node' та script path). Default — process.argv.slice(2).
|
|
253
262
|
* @returns {Promise<{changed: boolean, bump: string|null, version: string|null}>} результат main()
|
|
254
263
|
*/
|
|
@@ -256,27 +265,28 @@ export async function cli(argv = process.argv.slice(2)) {
|
|
|
256
265
|
const { values } = parseArgs({
|
|
257
266
|
args: argv,
|
|
258
267
|
options: {
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
endpoint: { type: 'string' },
|
|
269
|
+
header: { type: 'string', multiple: true, default: [] },
|
|
261
270
|
docs: { type: 'string', default: './docs' },
|
|
262
271
|
'schema-name': { type: 'string', default: 'maya.graphql' },
|
|
263
|
-
'
|
|
272
|
+
'source-ref': { type: 'string', default: 'unknown' }
|
|
264
273
|
},
|
|
265
274
|
allowPositionals: false,
|
|
266
275
|
strict: true
|
|
267
276
|
})
|
|
268
277
|
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
271
|
-
throw new Error('--
|
|
278
|
+
const endpoint = values.endpoint
|
|
279
|
+
if (!endpoint) {
|
|
280
|
+
throw new Error('--endpoint is required')
|
|
272
281
|
}
|
|
273
282
|
|
|
274
|
-
const
|
|
283
|
+
const headers = Object.fromEntries(values.header.map(h => parseHeader(h)))
|
|
284
|
+
const newSdl = await fetchSdl(endpoint, headers)
|
|
275
285
|
|
|
276
286
|
const result = await main({
|
|
277
287
|
newSdl,
|
|
278
288
|
docsRoot: values.docs,
|
|
279
|
-
|
|
289
|
+
sourceRef: values['source-ref'],
|
|
280
290
|
date: new Date().toISOString().slice(0, 10),
|
|
281
291
|
schemaFilename: values['schema-name']
|
|
282
292
|
})
|
package/types/cli.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Стейджить файли, робить commit + push. Якщо файли не несуть змін — нічого не комітить і не пушить.
|
|
3
|
+
* @param {{repo: string, files: string[], message: string, authorName: string, authorEmail: string, branch?: string, remote?: string}} params параметри
|
|
4
|
+
* @returns {{committed: boolean, sha: string|null}} результат: чи був коміт і його SHA
|
|
5
|
+
*/
|
|
6
|
+
export function main({
|
|
7
|
+
repo,
|
|
8
|
+
files,
|
|
9
|
+
message,
|
|
10
|
+
authorName,
|
|
11
|
+
authorEmail,
|
|
12
|
+
branch,
|
|
13
|
+
remote
|
|
14
|
+
}: {
|
|
15
|
+
repo: string
|
|
16
|
+
files: string[]
|
|
17
|
+
message: string
|
|
18
|
+
authorName: string
|
|
19
|
+
authorEmail: string
|
|
20
|
+
branch?: string
|
|
21
|
+
remote?: string
|
|
22
|
+
}): {
|
|
23
|
+
committed: boolean
|
|
24
|
+
sha: string | null
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* CLI-обгортка для commit-push. Параметри як `--key value`.
|
|
28
|
+
*
|
|
29
|
+
* Обовʼязкові:
|
|
30
|
+
* --repo <path> шлях до git-репо
|
|
31
|
+
* --message <msg> повідомлення коміту
|
|
32
|
+
* --file <path> повторюваний; шлях файлу від кореня репо (мінімум один)
|
|
33
|
+
* --author-name <name> Git user.name
|
|
34
|
+
* --author-email <email> Git user.email
|
|
35
|
+
*
|
|
36
|
+
* Необовʼязкові:
|
|
37
|
+
* --branch <name> цільова гілка (default 'main')
|
|
38
|
+
* --remote <name> remote (default 'origin')
|
|
39
|
+
* @param {string[]} [argv] аргументи. Default — process.argv.slice(2).
|
|
40
|
+
* @returns {{committed: boolean, sha: string|null}} результат main()
|
|
41
|
+
*/
|
|
42
|
+
export function cli(argv?: string[]): {
|
|
43
|
+
committed: boolean
|
|
44
|
+
sha: string | null
|
|
45
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Класифікує зміни схеми у секції changelog та визначає тип bump.
|
|
3
|
+
* @param {Array<{type?: string, message: string, criticality?: {level: string}}>} changes масив змін від graphql-inspector
|
|
4
|
+
* @returns {{bump: 'minor'|'patch'|null, sections: {added: string[], removed: string[], changed: string[]}}} тип bump та секції changelog
|
|
5
|
+
*/
|
|
6
|
+
export function classifyChanges(
|
|
7
|
+
changes: Array<{
|
|
8
|
+
type?: string
|
|
9
|
+
message: string
|
|
10
|
+
criticality?: {
|
|
11
|
+
level: string
|
|
12
|
+
}
|
|
13
|
+
}>
|
|
14
|
+
): {
|
|
15
|
+
bump: 'minor' | 'patch' | null
|
|
16
|
+
sections: {
|
|
17
|
+
added: string[]
|
|
18
|
+
removed: string[]
|
|
19
|
+
changed: string[]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Будує markdown-блок changelog для версії.
|
|
24
|
+
* @param {{version: string, date: string, sourceRef: string, sections: {added: string[], removed: string[], changed: string[]}, first?: boolean}} params параметри блоку
|
|
25
|
+
* @returns {string} markdown-блок з трейлінговим порожнім рядком
|
|
26
|
+
*/
|
|
27
|
+
export function formatChangelogBlock({
|
|
28
|
+
version,
|
|
29
|
+
date,
|
|
30
|
+
sourceRef,
|
|
31
|
+
sections,
|
|
32
|
+
first
|
|
33
|
+
}: {
|
|
34
|
+
version: string
|
|
35
|
+
date: string
|
|
36
|
+
sourceRef: string
|
|
37
|
+
sections: {
|
|
38
|
+
added: string[]
|
|
39
|
+
removed: string[]
|
|
40
|
+
changed: string[]
|
|
41
|
+
}
|
|
42
|
+
first?: boolean
|
|
43
|
+
}): string
|
|
44
|
+
/**
|
|
45
|
+
* Вставляє новий блок changelog перед першим існуючим записом (або після хедера, якщо записів нема).
|
|
46
|
+
* @param {string} existing вміст CHANGELOG.md
|
|
47
|
+
* @param {string} newBlock новий блок версії
|
|
48
|
+
* @returns {string} оновлений вміст
|
|
49
|
+
*/
|
|
50
|
+
export function prependChangelog(existing: string, newBlock: string): string
|
|
51
|
+
/**
|
|
52
|
+
* Запускає graphql-inspector diff між двома SDL.
|
|
53
|
+
* @param {string} oldSdl стара GraphQL-схема (SDL)
|
|
54
|
+
* @param {string} newSdl нова GraphQL-схема (SDL)
|
|
55
|
+
* @returns {Promise<Array<{type: string, message: string, criticality: {level: string}}>>} список змін
|
|
56
|
+
*/
|
|
57
|
+
export function runInspector(
|
|
58
|
+
oldSdl: string,
|
|
59
|
+
newSdl: string
|
|
60
|
+
): Promise<
|
|
61
|
+
Array<{
|
|
62
|
+
type: string
|
|
63
|
+
message: string
|
|
64
|
+
criticality: {
|
|
65
|
+
level: string
|
|
66
|
+
}
|
|
67
|
+
}>
|
|
68
|
+
>
|
|
69
|
+
/**
|
|
70
|
+
* Читає SDL з файлу або повертає null, якщо файл відсутній/порожній.
|
|
71
|
+
* @param {string} path шлях до SDL
|
|
72
|
+
* @returns {string|null} вміст або null
|
|
73
|
+
*/
|
|
74
|
+
export function readSdl(path: string): string | null
|
|
75
|
+
/**
|
|
76
|
+
* Підіймає версію у package.json напряму (без виклику зовнішнього `npm`).
|
|
77
|
+
* @param {string} npmDir шлях до директорії пакета
|
|
78
|
+
* @param {'major'|'minor'|'patch'} kind тип bump
|
|
79
|
+
* @returns {string} нова версія
|
|
80
|
+
*/
|
|
81
|
+
export function bumpVersion(npmDir: string, kind: 'major' | 'minor' | 'patch'): string
|
|
82
|
+
/**
|
|
83
|
+
* Пише пари key=value у файл, на який вказує `GITHUB_OUTPUT`.
|
|
84
|
+
* @param {Record<string, string>} values пари для запису
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
export function writeGithubOutput(values: Record<string, string>): void
|
|
88
|
+
/**
|
|
89
|
+
* Шле introspection-запит до GraphQL-ендпоінта і повертає SDL-рядок.
|
|
90
|
+
* @param {string} endpoint URL GraphQL-ендпоінта
|
|
91
|
+
* @param {Record<string, string>} [headers] додаткові HTTP-заголовки (наприклад `{ 'X-Hasura-Admin-Secret': '...' }`)
|
|
92
|
+
* @returns {Promise<string>} SDL-схема у вигляді рядка
|
|
93
|
+
*/
|
|
94
|
+
export function fetchSdl(endpoint: string, headers?: Record<string, string>): Promise<string>
|
|
95
|
+
/**
|
|
96
|
+
* Парсить рядок `Key: Value` у пару `[key, value]`. Помилка, якщо нема `:`.
|
|
97
|
+
* @param {string} raw сирий header
|
|
98
|
+
* @returns {[string, string]} key, value (обрізані з обох боків)
|
|
99
|
+
*/
|
|
100
|
+
export function parseHeader(raw: string): [string, string]
|
|
101
|
+
/**
|
|
102
|
+
* Оркеструє весь flow: diff схем → bump → CHANGELOG → запис SDL.
|
|
103
|
+
* @param {{newSdl: string, docsRoot: string, sourceRef: string, date: string, schemaFilename?: string}} params параметри запуску
|
|
104
|
+
* @returns {Promise<{changed: boolean, bump: 'minor'|'patch'|null, version: string|null}>} результат
|
|
105
|
+
*/
|
|
106
|
+
export function main({
|
|
107
|
+
newSdl,
|
|
108
|
+
docsRoot,
|
|
109
|
+
sourceRef,
|
|
110
|
+
date,
|
|
111
|
+
schemaFilename
|
|
112
|
+
}: {
|
|
113
|
+
newSdl: string
|
|
114
|
+
docsRoot: string
|
|
115
|
+
sourceRef: string
|
|
116
|
+
date: string
|
|
117
|
+
schemaFilename?: string
|
|
118
|
+
}): Promise<{
|
|
119
|
+
changed: boolean
|
|
120
|
+
bump: 'minor' | 'patch' | null
|
|
121
|
+
version: string | null
|
|
122
|
+
}>
|
|
123
|
+
/**
|
|
124
|
+
* CLI-обгортка для sync-schema. Приймає параметри як `--key value`.
|
|
125
|
+
*
|
|
126
|
+
* Обовʼязковий:
|
|
127
|
+
* --endpoint <url> URL GraphQL-ендпоінта для introspection
|
|
128
|
+
*
|
|
129
|
+
* Необовʼязкові:
|
|
130
|
+
* --header "K: V" HTTP-заголовок (повторюваний; наприклад: `--header "X-Hasura-Admin-Secret: ..."`,
|
|
131
|
+
* `--header "Authorization: Bearer ..."`)
|
|
132
|
+
* --docs <path> корінь docs-репо (default './docs')
|
|
133
|
+
* --schema-name <file> назва файлу в `npm/schema/` (default 'maya.graphql')
|
|
134
|
+
* --source-ref <ref> текст, що йде у CHANGELOG як посилання на джерело (default 'unknown')
|
|
135
|
+
* @param {string[]} [argv] аргументи (без 'node' та script path). Default — process.argv.slice(2).
|
|
136
|
+
* @returns {Promise<{changed: boolean, bump: string|null, version: string|null}>} результат main()
|
|
137
|
+
*/
|
|
138
|
+
export function cli(argv?: string[]): Promise<{
|
|
139
|
+
changed: boolean
|
|
140
|
+
bump: string | null
|
|
141
|
+
version: string | null
|
|
142
|
+
}>
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'bun:test'
|
|
2
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { buildSchema, graphqlSync } from 'graphql'
|
|
6
|
-
import { classifyChanges, cli, formatChangelogBlock, main, prependChangelog, runInspector } from './main.mjs'
|
|
7
|
-
|
|
8
|
-
const HEADER = `# Changelog\n\nУсі помітні зміни цього пакета документуються тут.\n\nФормат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).\n\n`
|
|
9
|
-
const OLD_SDL = readFileSync(join(import.meta.dir, '__fixtures__/old-schema.graphql'), 'utf8')
|
|
10
|
-
const NEW_SDL = readFileSync(join(import.meta.dir, '__fixtures__/new-schema.graphql'), 'utf8')
|
|
11
|
-
|
|
12
|
-
describe('classifyChanges', () => {
|
|
13
|
-
it('повертає bump=null коли немає змін', () => {
|
|
14
|
-
expect(classifyChanges([])).toEqual({ bump: null, sections: { added: [], removed: [], changed: [] } })
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('повертає bump=patch для NON_BREAKING додавань', () => {
|
|
18
|
-
const result = classifyChanges([
|
|
19
|
-
{ type: 'FIELD_ADDED', criticality: { level: 'NON_BREAKING' }, message: "Field 'foo' was added to type 'Bar'" }
|
|
20
|
-
])
|
|
21
|
-
expect(result.bump).toBe('patch')
|
|
22
|
-
expect(result.sections.added).toEqual(["Field 'foo' was added to type 'Bar'"])
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('повертає bump=minor для будь-якого BREAKING', () => {
|
|
26
|
-
const result = classifyChanges([
|
|
27
|
-
{ type: 'FIELD_REMOVED', criticality: { level: 'BREAKING' }, message: "Field 'old' was removed from type 'User'" },
|
|
28
|
-
{ type: 'FIELD_ADDED', criticality: { level: 'NON_BREAKING' }, message: "Field 'new' was added to type 'User'" }
|
|
29
|
-
])
|
|
30
|
-
expect(result.bump).toBe('minor')
|
|
31
|
-
expect(result.sections.removed).toEqual(["Field 'old' was removed from type 'User'"])
|
|
32
|
-
expect(result.sections.added).toEqual(["Field 'new' was added to type 'User'"])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('повертає bump=patch для DANGEROUS', () => {
|
|
36
|
-
const result = classifyChanges([
|
|
37
|
-
{ type: 'FIELD_ARGUMENT_DEFAULT_CHANGED', criticality: { level: 'DANGEROUS' }, message: "Default for arg 'limit' changed" }
|
|
38
|
-
])
|
|
39
|
-
expect(result.bump).toBe('patch')
|
|
40
|
-
expect(result.sections.changed).toEqual(["Default for arg 'limit' changed"])
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('класифікує BREAKING модифікації у `changed`', () => {
|
|
44
|
-
const result = classifyChanges([
|
|
45
|
-
{ type: 'TYPE_KIND_CHANGED', criticality: { level: 'BREAKING' }, message: "Type 'Foo' changed kind" }
|
|
46
|
-
])
|
|
47
|
-
expect(result.sections.changed).toEqual(["Type 'Foo' changed kind"])
|
|
48
|
-
expect(result.sections.removed).toEqual([])
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('formatChangelogBlock', () => {
|
|
53
|
-
const baseInput = { version: '0.1.0', date: '2026-05-11', dbSha: 'a1b2c3d', sections: { added: [], removed: [], changed: [] } }
|
|
54
|
-
|
|
55
|
-
it('завжди містить "Changed" з посиланням на db-SHA', () => {
|
|
56
|
-
const out = formatChangelogBlock(baseInput)
|
|
57
|
-
expect(out).toContain('## [0.1.0] - 2026-05-11')
|
|
58
|
-
expect(out).toContain('### Changed')
|
|
59
|
-
expect(out).toContain('Оновлено GraphQL-схему з Hasura (`db@a1b2c3d`)')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('додає секцію Removed для breaking-видалень', () => {
|
|
63
|
-
const out = formatChangelogBlock({ ...baseInput, sections: { added: [], removed: ["Field 'old' removed"], changed: [] } })
|
|
64
|
-
expect(out).toContain('### Removed')
|
|
65
|
-
expect(out).toContain("- Field 'old' removed")
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('додає секцію Added для non-breaking додавань', () => {
|
|
69
|
-
const out = formatChangelogBlock({ ...baseInput, sections: { added: ["Field 'new' added"], removed: [], changed: [] } })
|
|
70
|
-
expect(out).toContain('### Added')
|
|
71
|
-
expect(out).toContain("- Field 'new' added")
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('не друкує порожні секції', () => {
|
|
75
|
-
const out = formatChangelogBlock(baseInput)
|
|
76
|
-
expect(out).not.toContain('### Added')
|
|
77
|
-
expect(out).not.toContain('### Removed')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('first-run шаблон, коли first=true', () => {
|
|
81
|
-
const out = formatChangelogBlock({ ...baseInput, first: true })
|
|
82
|
-
expect(out).toContain('Початкове додавання GraphQL-схеми')
|
|
83
|
-
expect(out).not.toContain('Оновлено GraphQL-схему')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('закінчується одним порожнім рядком (для коректного prepend)', () => {
|
|
87
|
-
const out = formatChangelogBlock(baseInput)
|
|
88
|
-
expect(out.endsWith('\n\n')).toBe(true)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
describe('runInspector', () => {
|
|
93
|
-
it('повертає порожній масив для ідентичних схем', async () => {
|
|
94
|
-
const sdl = `type Query { hello: String }`
|
|
95
|
-
const changes = await runInspector(sdl, sdl)
|
|
96
|
-
expect(changes).toEqual([])
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('детектить BREAKING при видаленні поля', async () => {
|
|
100
|
-
const oldSdl = `type Query { hello: String, bye: String }`
|
|
101
|
-
const newSdl = `type Query { hello: String }`
|
|
102
|
-
const changes = await runInspector(oldSdl, newSdl)
|
|
103
|
-
const breaking = changes.find(c => c.criticality.level === 'BREAKING')
|
|
104
|
-
expect(breaking).toBeDefined()
|
|
105
|
-
expect(breaking.type).toBe('FIELD_REMOVED')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('детектить NON_BREAKING при додаванні поля', async () => {
|
|
109
|
-
const oldSdl = `type Query { hello: String }`
|
|
110
|
-
const newSdl = `type Query { hello: String, newField: Int }`
|
|
111
|
-
const changes = await runInspector(oldSdl, newSdl)
|
|
112
|
-
const nb = changes.find(c => c.type === 'FIELD_ADDED')
|
|
113
|
-
expect(nb).toBeDefined()
|
|
114
|
-
expect(nb.criticality.level).toBe('NON_BREAKING')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
describe('prependChangelog', () => {
|
|
119
|
-
it('вставляє новий блок між хедером і першим існуючим записом', () => {
|
|
120
|
-
const existing = HEADER + '## [0.0.2] - 2026-05-11\n\n### Added\n\n- щось.\n'
|
|
121
|
-
const block = '## [0.0.3] - 2026-05-12\n\n### Changed\n\n- нове.\n\n'
|
|
122
|
-
const out = prependChangelog(existing, block)
|
|
123
|
-
expect(out.indexOf('## [0.0.3]')).toBeLessThan(out.indexOf('## [0.0.2]'))
|
|
124
|
-
expect(out.startsWith('# Changelog')).toBe(true)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('додає блок після хедера, якщо записів ще нема', () => {
|
|
128
|
-
const block = '## [0.0.1] - 2026-05-11\n\n### Added\n\n- перший запис.\n\n'
|
|
129
|
-
const out = prependChangelog(HEADER, block)
|
|
130
|
-
expect(out).toBe(HEADER + block)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('main (e2e via fixtures)', () => {
|
|
135
|
-
let tmp
|
|
136
|
-
|
|
137
|
-
function setupTmpDocs({ withOldSchema = true, schemaFilename = 'maya.graphql' } = {}) {
|
|
138
|
-
tmp = mkdtempSync(join(tmpdir(), 'sync-schema-'))
|
|
139
|
-
const npmDir = join(tmp, 'npm')
|
|
140
|
-
mkdirSync(npmDir, { recursive: true })
|
|
141
|
-
writeFileSync(join(npmDir, 'package.json'), JSON.stringify({ name: '@nitra/efes-docs', version: '0.0.2' }, null, 2))
|
|
142
|
-
writeFileSync(join(npmDir, 'CHANGELOG.md'), HEADER + '## [0.0.2] - 2026-05-10\n\n### Added\n\n- Базовий каркас.\n')
|
|
143
|
-
if (withOldSchema) {
|
|
144
|
-
mkdirSync(join(npmDir, 'schema'), { recursive: true })
|
|
145
|
-
writeFileSync(join(npmDir, 'schema', schemaFilename), OLD_SDL)
|
|
146
|
-
}
|
|
147
|
-
return { docsRoot: tmp, npmDir }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
afterEach(() => {
|
|
151
|
-
if (tmp && existsSync(tmp)) rmSync(tmp, { recursive: true, force: true })
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('NON_BREAKING зміна → bump patch, CHANGELOG.Added', async () => {
|
|
155
|
-
const { docsRoot, npmDir } = setupTmpDocs()
|
|
156
|
-
const result = await main({ newSdl: NEW_SDL, docsRoot, dbSha: 'abc1234567', date: '2026-05-11' })
|
|
157
|
-
console.log(JSON.stringify(result, null, 2))
|
|
158
|
-
|
|
159
|
-
expect(result.changed).toBe(true)
|
|
160
|
-
expect(result.version).toBe('0.0.3')
|
|
161
|
-
|
|
162
|
-
const changelog = readFileSync(join(npmDir, 'CHANGELOG.md'), 'utf8')
|
|
163
|
-
expect(changelog).toContain('## [0.0.3] - 2026-05-11')
|
|
164
|
-
expect(changelog.indexOf('## [0.0.3]')).toBeLessThan(changelog.indexOf('## [0.0.2]'))
|
|
165
|
-
|
|
166
|
-
const schema = readFileSync(join(npmDir, 'schema/maya.graphql'), 'utf8')
|
|
167
|
-
expect(schema).toBe(NEW_SDL)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('однакові схеми → changed=false, нічого не записує', async () => {
|
|
171
|
-
const { docsRoot, npmDir } = setupTmpDocs()
|
|
172
|
-
const result = await main({ newSdl: OLD_SDL, docsRoot, dbSha: 'abc1234567', date: '2026-05-11' })
|
|
173
|
-
console.log(JSON.stringify(result, null, 2))
|
|
174
|
-
|
|
175
|
-
expect(result.changed).toBe(false)
|
|
176
|
-
expect(result.bump).toBeNull()
|
|
177
|
-
|
|
178
|
-
const pkg = JSON.parse(readFileSync(join(npmDir, 'package.json'), 'utf8'))
|
|
179
|
-
expect(pkg.version).toBe('0.0.2')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('first-run (нема старої схеми) → bump patch, CHANGELOG з "Початкове додавання"', async () => {
|
|
183
|
-
const { docsRoot, npmDir } = setupTmpDocs({ withOldSchema: false })
|
|
184
|
-
const result = await main({ newSdl: NEW_SDL, docsRoot, dbSha: 'abc1234567', date: '2026-05-11' })
|
|
185
|
-
console.log(JSON.stringify(result, null, 2))
|
|
186
|
-
|
|
187
|
-
expect(result.changed).toBe(true)
|
|
188
|
-
expect(result.bump).toBe('patch')
|
|
189
|
-
expect(result.version).toBe('0.0.3')
|
|
190
|
-
|
|
191
|
-
const changelog = readFileSync(join(npmDir, 'CHANGELOG.md'), 'utf8')
|
|
192
|
-
expect(changelog).toContain('Початкове додавання GraphQL-схеми')
|
|
193
|
-
expect(existsSync(join(npmDir, 'schema/maya.graphql'))).toBe(true)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('кастомний schemaFilename (smart.graphql) — записує саме його', async () => {
|
|
197
|
-
const { docsRoot, npmDir } = setupTmpDocs({ schemaFilename: 'smart.graphql' })
|
|
198
|
-
const result = await main({ newSdl: NEW_SDL, docsRoot, dbSha: 'abc1234567', date: '2026-05-11', schemaFilename: 'smart.graphql' })
|
|
199
|
-
|
|
200
|
-
expect(result.changed).toBe(true)
|
|
201
|
-
expect(existsSync(join(npmDir, 'schema/smart.graphql'))).toBe(true)
|
|
202
|
-
expect(existsSync(join(npmDir, 'schema/maya.graphql'))).toBe(false)
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
describe('cli (тільки args)', () => {
|
|
207
|
-
let tmp
|
|
208
|
-
let server
|
|
209
|
-
let receivedHeaders
|
|
210
|
-
|
|
211
|
-
function setupBareDocs() {
|
|
212
|
-
tmp = mkdtempSync(join(tmpdir(), 'sync-schema-cli-'))
|
|
213
|
-
const npmDir = join(tmp, 'npm')
|
|
214
|
-
mkdirSync(npmDir, { recursive: true })
|
|
215
|
-
writeFileSync(join(npmDir, 'package.json'), JSON.stringify({ name: '@nitra/x-docs', version: '0.0.2' }, null, 2))
|
|
216
|
-
writeFileSync(join(npmDir, 'CHANGELOG.md'), HEADER + '## [0.0.2] - 2026-05-10\n\n### Added\n\n- щось.\n')
|
|
217
|
-
return tmp
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function startMockHasura(sdl) {
|
|
221
|
-
const schema = buildSchema(sdl)
|
|
222
|
-
server = Bun.serve({
|
|
223
|
-
port: 0,
|
|
224
|
-
async fetch(req) {
|
|
225
|
-
receivedHeaders = req.headers
|
|
226
|
-
const { query } = await req.json()
|
|
227
|
-
const result = graphqlSync({ schema, source: query })
|
|
228
|
-
return Response.json(result)
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
return `http://localhost:${server.port}/v1/graphql`
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
afterEach(() => {
|
|
235
|
-
if (server) {
|
|
236
|
-
server.stop()
|
|
237
|
-
server = undefined
|
|
238
|
-
}
|
|
239
|
-
receivedHeaders = undefined
|
|
240
|
-
if (tmp && existsSync(tmp)) rmSync(tmp, { recursive: true, force: true })
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('інтроспектить Hasura через --hasura-url і синкає схему', async () => {
|
|
244
|
-
const docsRoot = setupBareDocs()
|
|
245
|
-
const url = startMockHasura(NEW_SDL)
|
|
246
|
-
const result = await cli(['--hasura-url', url, '--docs', docsRoot, '--schema-name', 'test.graphql', '--db-sha', 'abcdef1234'])
|
|
247
|
-
|
|
248
|
-
expect(result.changed).toBe(true)
|
|
249
|
-
expect(result.bump).toBe('patch')
|
|
250
|
-
expect(existsSync(join(docsRoot, 'npm', 'schema', 'test.graphql'))).toBe(true)
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('передає --hasura-secret як X-Hasura-Admin-Secret заголовок', async () => {
|
|
254
|
-
const docsRoot = setupBareDocs()
|
|
255
|
-
const url = startMockHasura(NEW_SDL)
|
|
256
|
-
await cli(['--hasura-url', url, '--hasura-secret', 'super-secret', '--docs', docsRoot])
|
|
257
|
-
|
|
258
|
-
expect(receivedHeaders.get('x-hasura-admin-secret')).toBe('super-secret')
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('без --hasura-secret заголовок не надсилається', async () => {
|
|
262
|
-
const docsRoot = setupBareDocs()
|
|
263
|
-
const url = startMockHasura(NEW_SDL)
|
|
264
|
-
await cli(['--hasura-url', url, '--docs', docsRoot])
|
|
265
|
-
|
|
266
|
-
expect(receivedHeaders.get('x-hasura-admin-secret')).toBeNull()
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('схема за замовчуванням — maya.graphql', async () => {
|
|
270
|
-
const docsRoot = setupBareDocs()
|
|
271
|
-
const url = startMockHasura(NEW_SDL)
|
|
272
|
-
await cli(['--hasura-url', url, '--docs', docsRoot])
|
|
273
|
-
|
|
274
|
-
expect(existsSync(join(docsRoot, 'npm', 'schema', 'maya.graphql'))).toBe(true)
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('викидає якщо не передано --hasura-url', () => {
|
|
278
|
-
expect(cli(['--docs', '/tmp/nowhere'])).rejects.toThrow('--hasura-url is required')
|
|
279
|
-
})
|
|
280
|
-
})
|
package/types/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|