v8scli 0.2.2 → 0.4.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/README.md +18 -0
- package/bin/v8s.js +9 -1
- package/package.json +1 -1
- package/src/commands/build.js +5 -0
- package/src/commands/mod.js +168 -0
- package/src/commands/upload.js +7 -2
- package/src/typegen.js +170 -0
- package/templates/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,24 @@ npx v8scli update # bump pinned releases to the latest
|
|
|
23
23
|
npx v8scli deps # (re)download typings of dependencies
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
## Privileges and commands (`apiVersion: 2`)
|
|
27
|
+
|
|
28
|
+
For a manifest with `"apiVersion": 2`, `build` and `upload` write `xplay.generated.ts` next to the
|
|
29
|
+
entry: typed keys from `mod.json` (`PermissionKey`, `PlayerValues`, `SessionSettings`, `CommandKey`,
|
|
30
|
+
`CommandArgs`) and ready `access` / `commands` objects from `v8_scripting/access`. Then the project
|
|
31
|
+
is typechecked (`tsc --noEmit`) — a mistyped key or a player key read as a server setting fails the
|
|
32
|
+
build, not the game. The file is part of the sources: commit it, `upload` packs it.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { access, commands } from './xplay.generated';
|
|
36
|
+
import { playerRef } from 'v8_scripting/access';
|
|
37
|
+
|
|
38
|
+
const ref = playerRef(controller);
|
|
39
|
+
if (access.permission('use').can(ref)) { /* ... */ }
|
|
40
|
+
const limit = access.value('knives_limit').get(ref) ?? 3;
|
|
41
|
+
commands.on('refill', (ctx, args) => `refilled ${args.target}`);
|
|
42
|
+
```
|
|
43
|
+
|
|
26
44
|
## Local SDK/CLI development
|
|
27
45
|
|
|
28
46
|
`--local` wires the SDK and CLI as `file:` dependencies from your checkouts
|
package/bin/v8s.js
CHANGED
|
@@ -9,14 +9,22 @@ import { deps } from '../src/commands/deps.js';
|
|
|
9
9
|
import { pull } from '../src/commands/pull.js';
|
|
10
10
|
import { search } from '../src/commands/search.js';
|
|
11
11
|
import { add, remove, update } from '../src/commands/add.js';
|
|
12
|
+
import { create, edit, icon, cover, submit, release, whoami } from '../src/commands/mod.js';
|
|
12
13
|
|
|
13
14
|
const [, , command, ...args] = process.argv;
|
|
14
15
|
|
|
15
|
-
const commands = { init, build, login, upload, status, deps, pull, search, add, remove, update
|
|
16
|
+
const commands = { init, build, login, upload, status, deps, pull, search, add, remove, update,
|
|
17
|
+
create, edit, icon, cover, submit, release, whoami };
|
|
16
18
|
|
|
17
19
|
const usage = `v8scli <command>
|
|
18
20
|
|
|
19
21
|
init <slug> [--local] scaffold a new mod in ./<slug> (--local: SDK/CLI from checkouts)
|
|
22
|
+
create [--library] register the mod from ./mod.json on the portal
|
|
23
|
+
edit <flags> change name/description/visibility (--name, --description, --full <file>, --visibility)
|
|
24
|
+
icon <file> upload the square icon cover <file> upload the 16:9 cover
|
|
25
|
+
submit send the latest uploaded version to dev review
|
|
26
|
+
release "notes" publish the latest version to prod (admin approves)
|
|
27
|
+
whoami show whose token is in use
|
|
20
28
|
build bundle the mod locally (self-check, dist/<slug>.js)
|
|
21
29
|
login save portal URL and API token (~/.config/v8s/config.json)
|
|
22
30
|
upload pack sources and upload a new version to the portal
|
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readManifest } from '../common.js';
|
|
4
|
+
import { ensureGenerated, typecheck } from '../typegen.js';
|
|
4
5
|
|
|
5
6
|
// Локальная самопроверка: тот же контракт, что на портале —
|
|
6
7
|
// только файлы мода + SDK, cs_script/point_script остаётся external.
|
|
@@ -9,6 +10,10 @@ export async function build() {
|
|
|
9
10
|
const manifest = await readManifest();
|
|
10
11
|
const disallowed = new Set();
|
|
11
12
|
|
|
13
|
+
// apiVersion 2: ключи мода — типами из mod.json, и сборка без проверки типов не идёт
|
|
14
|
+
if (await ensureGenerated(manifest))
|
|
15
|
+
typecheck();
|
|
16
|
+
|
|
12
17
|
const result = await esbuild.build({
|
|
13
18
|
entryPoints: [manifest.entry],
|
|
14
19
|
bundle: true,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
3
|
+
import { api, readConfig, readManifest } from '../common.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Жизненный цикл мода из CLI: создать, описать, дать картинки, отправить.
|
|
7
|
+
*
|
|
8
|
+
* До этого создание и оформление жили только в веб-панели, и написать мод
|
|
9
|
+
* целиком из редактора было нельзя — приходилось прерываться на браузер.
|
|
10
|
+
* Все команды работают по API-токену.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** modId по slug из mod.json; вызывающему нужен и сам манифест */
|
|
14
|
+
async function resolveMod(config, manifest) {
|
|
15
|
+
const { mods } = await api(config, '/api/mods/my');
|
|
16
|
+
const mod = mods.find((m) => m.slug === manifest.slug);
|
|
17
|
+
if (!mod) throw new Error(`mod "${manifest.slug}" not found on the portal — run \`v8scli create\` first`);
|
|
18
|
+
return mod;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Последняя версия мода — с ней работают submit и release */
|
|
22
|
+
async function latestVersion(config, modId) {
|
|
23
|
+
const { versions } = await api(config, `/api/mods/${modId}`);
|
|
24
|
+
if (!versions?.length) throw new Error('no versions uploaded yet — run `v8scli upload` first');
|
|
25
|
+
return versions[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
29
|
+
|
|
30
|
+
async function uploadImage(config, modId, path, kind) {
|
|
31
|
+
const body = await readFile(path);
|
|
32
|
+
const type = MIME[extname(path).toLowerCase()];
|
|
33
|
+
if (!type) throw new Error(`unsupported image type: ${extname(path)} (png, jpg, webp)`);
|
|
34
|
+
|
|
35
|
+
const form = new FormData();
|
|
36
|
+
form.append('file', new Blob([body], { type }), basename(path));
|
|
37
|
+
|
|
38
|
+
const { mod } = await api(config, `/api/mods/${modId}/${kind}`, { method: 'POST', body: form });
|
|
39
|
+
console.log(`${kind}: ${kind === 'icon' ? mod.iconUrl : mod.coverUrl}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** v8scli create — заводит мод на портале по mod.json текущей папки */
|
|
43
|
+
export async function create(args) {
|
|
44
|
+
const config = await readConfig();
|
|
45
|
+
const manifest = await readManifest();
|
|
46
|
+
|
|
47
|
+
const isLibrary = args.includes('--library') || Boolean(manifest.library);
|
|
48
|
+
|
|
49
|
+
const { mod } = await api(config, '/api/mods/create', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
slug: manifest.slug,
|
|
54
|
+
name: typeof manifest.name === 'string' ? manifest.name : manifest.slug,
|
|
55
|
+
description: typeof manifest.description === 'string' ? manifest.description : '',
|
|
56
|
+
isLibrary,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log(`created: ${mod.slug} (#${mod.id})${isLibrary ? ' [library]' : ''}`);
|
|
61
|
+
console.log('next: v8scli upload');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** v8scli edit --name … --description … --full FILE --visibility … */
|
|
65
|
+
export async function edit(args) {
|
|
66
|
+
const config = await readConfig();
|
|
67
|
+
const manifest = await readManifest();
|
|
68
|
+
const mod = await resolveMod(config, manifest);
|
|
69
|
+
|
|
70
|
+
const value = (flag) => {
|
|
71
|
+
const at = args.indexOf(flag);
|
|
72
|
+
return at >= 0 ? args[at + 1] : undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const body = {};
|
|
76
|
+
if (value('--name')) body.name = value('--name');
|
|
77
|
+
if (value('--description')) body.description = value('--description');
|
|
78
|
+
if (value('--visibility')) body.visibility = value('--visibility');
|
|
79
|
+
if (value('--full')) body.descriptionFull = await readFile(value('--full'), 'utf-8');
|
|
80
|
+
|
|
81
|
+
if (!Object.keys(body).length)
|
|
82
|
+
throw new Error('nothing to change: --name, --description, --full <file>, --visibility <public|whitelist|private>');
|
|
83
|
+
|
|
84
|
+
const { mod: updated } = await api(config, `/api/mods/${mod.id}/update`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`updated: ${updated.slug} — ${Object.keys(body).join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** v8scli icon <file> / v8scli cover <file> */
|
|
94
|
+
export async function icon(args) {
|
|
95
|
+
const config = await readConfig();
|
|
96
|
+
const mod = await resolveMod(config, await readManifest());
|
|
97
|
+
if (!args[0]) throw new Error('usage: v8scli icon <file.png>');
|
|
98
|
+
await uploadImage(config, mod.id, args[0], 'icon');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function cover(args) {
|
|
102
|
+
const config = await readConfig();
|
|
103
|
+
const mod = await resolveMod(config, await readManifest());
|
|
104
|
+
if (!args[0]) throw new Error('usage: v8scli cover <file.png>');
|
|
105
|
+
await uploadImage(config, mod.id, args[0], 'cover');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** v8scli submit — последнюю загруженную версию на дев-проверку */
|
|
109
|
+
export async function submit() {
|
|
110
|
+
const config = await readConfig();
|
|
111
|
+
const mod = await resolveMod(config, await readManifest());
|
|
112
|
+
const version = await latestVersion(config, mod.id);
|
|
113
|
+
|
|
114
|
+
// У разработчиков команды загрузка сразу даёт dev_ready — отправлять нечего,
|
|
115
|
+
// и это нормальный исход, а не ошибка: сценарий «залил и тестирую» не ломается
|
|
116
|
+
if (version.status === 'dev_ready') {
|
|
117
|
+
console.log(`${version.version}: already dev_ready — ready to test in your lobby`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (version.status !== 'draft')
|
|
122
|
+
throw new Error(`version ${version.version} is ${version.status}, nothing to submit`);
|
|
123
|
+
|
|
124
|
+
const { version: moved } = await api(config, `/api/mods/${mod.id}/versions/${version.id}/action`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ action: 'submit_dev' }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// У разработчиков команды дев-ревью пропускается, и версия сразу dev_ready
|
|
131
|
+
console.log(`${moved.version}: ${moved.status}`);
|
|
132
|
+
console.log(moved.status === 'dev_ready'
|
|
133
|
+
? 'ready to test in your lobby'
|
|
134
|
+
: 'sent to review');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** v8scli release "заметки к релизу" — публикация версии в прод */
|
|
138
|
+
export async function release(args) {
|
|
139
|
+
const config = await readConfig();
|
|
140
|
+
const mod = await resolveMod(config, await readManifest());
|
|
141
|
+
const version = await latestVersion(config, mod.id);
|
|
142
|
+
|
|
143
|
+
const notes = args.join(' ').trim();
|
|
144
|
+
if (notes.length < 10)
|
|
145
|
+
throw new Error('release notes are required (10 characters or more): v8scli release "what changed"');
|
|
146
|
+
|
|
147
|
+
if (version.status === 'draft')
|
|
148
|
+
throw new Error(`version ${version.version} is still a draft — run \`v8scli submit\` first`);
|
|
149
|
+
|
|
150
|
+
const { version: moved } = await api(config, `/api/mods/${mod.id}/versions/${version.id}/action`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ action: 'submit_release', changelog: notes }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`${moved.version}: ${moved.status} — waiting for an admin to publish it`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** v8scli whoami — чей это токен и можно ли им работать */
|
|
160
|
+
export async function whoami() {
|
|
161
|
+
const config = await readConfig();
|
|
162
|
+
const { developer } = await api(config, '/api/developers/me');
|
|
163
|
+
|
|
164
|
+
if (!developer) throw new Error('token is valid but the account is not a developer yet');
|
|
165
|
+
|
|
166
|
+
console.log(`${developer.nickname} (${developer.accountId}) — ${developer.status}`);
|
|
167
|
+
console.log(`portal: ${config.portalUrl.replace(/\/\/[^@]*@/, '//')}`);
|
|
168
|
+
}
|
package/src/commands/upload.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { api, readConfig, readManifest } from '../common.js';
|
|
3
|
+
import { ensureGenerated, typecheck } from '../typegen.js';
|
|
3
4
|
|
|
4
5
|
// Пакует исходники (mod.json + src/ + typings) и создаёт версию на портале.
|
|
5
6
|
// Финальный артефакт собирает сам портал — загружаются только исходники.
|
|
@@ -41,11 +42,15 @@ export async function upload() {
|
|
|
41
42
|
const config = await readConfig();
|
|
42
43
|
const manifest = await readManifest();
|
|
43
44
|
|
|
45
|
+
// Сгенерированные типы — часть исходников: портал собирает мод с тем же модулем, что и build
|
|
46
|
+
if (await ensureGenerated(manifest))
|
|
47
|
+
typecheck();
|
|
48
|
+
|
|
44
49
|
// modId по slug
|
|
45
50
|
const { mods } = await api(config, '/api/mods/my');
|
|
46
51
|
const mod = mods.find((m) => m.slug === manifest.slug);
|
|
47
52
|
if (!mod)
|
|
48
|
-
throw new Error(`mod "${manifest.slug}" not found on the portal —
|
|
53
|
+
throw new Error(`mod "${manifest.slug}" not found on the portal — run \`v8scli create\` first`);
|
|
49
54
|
|
|
50
55
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
51
56
|
const archive = await packSources(manifest);
|
|
@@ -59,7 +64,7 @@ export async function upload() {
|
|
|
59
64
|
body: form,
|
|
60
65
|
});
|
|
61
66
|
console.log(`uploaded: ${manifest.slug}@${version.version} status=${version.status}`);
|
|
62
|
-
console.log('next: submit
|
|
67
|
+
console.log('next: v8scli submit');
|
|
63
68
|
return;
|
|
64
69
|
} catch (error) {
|
|
65
70
|
const match = String(error?.message ?? '').match(/version_not_incremented: last is (\d+\.\d+\.\d+)/);
|
package/src/typegen.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Типы ключей мода из mod.json — API apiVersion 2 (v8_scripting/access).
|
|
7
|
+
//
|
|
8
|
+
// Мод спрашивает у платформы разрешения, персональные значения и настройки сервера по ключам из
|
|
9
|
+
// манифеста. Чтобы опечатка в ключе или чтение player-ключа как настройки сервера ловились при сборке,
|
|
10
|
+
// а не в игре, CLI пишет рядом с entry модуль с типами и готовыми access / commands. Файл —
|
|
11
|
+
// часть исходников: его пакует upload, и портал собирает мод с ним же.
|
|
12
|
+
|
|
13
|
+
export const GENERATED_FILE = 'xplay.generated.ts';
|
|
14
|
+
|
|
15
|
+
const header = `// Сгенерировано v8scli из mod.json — не править руками: build и upload перепишут.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function literal(value) {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function union(values, fallback) {
|
|
23
|
+
const items = (values ?? []).filter((value) => typeof value === 'string');
|
|
24
|
+
return items.length ? items.map(literal).join(' | ') : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Тип значения настройки манифеста в TypeScript */
|
|
28
|
+
function valueType(setting) {
|
|
29
|
+
switch (setting.type) {
|
|
30
|
+
case 'bool':
|
|
31
|
+
return 'boolean';
|
|
32
|
+
case 'int':
|
|
33
|
+
case 'float':
|
|
34
|
+
case 'player':
|
|
35
|
+
case 'team':
|
|
36
|
+
return 'number';
|
|
37
|
+
case 'enum':
|
|
38
|
+
return union(setting.variants, 'string');
|
|
39
|
+
case 'set':
|
|
40
|
+
return `Array<${union(setting.variants, 'string')}>`;
|
|
41
|
+
case 'players':
|
|
42
|
+
return 'number[]';
|
|
43
|
+
case 'audience':
|
|
44
|
+
return 'AudienceValue';
|
|
45
|
+
case 'string':
|
|
46
|
+
case 'color':
|
|
47
|
+
default:
|
|
48
|
+
return 'string';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Все настройки, включая зависимые (dependents), в порядке манифеста */
|
|
53
|
+
function flattenSettings(settings, out = []) {
|
|
54
|
+
for (const setting of settings ?? []) {
|
|
55
|
+
if (!setting || typeof setting.key !== 'string')
|
|
56
|
+
continue;
|
|
57
|
+
out.push(setting);
|
|
58
|
+
if (Array.isArray(setting.dependents))
|
|
59
|
+
flattenSettings(setting.dependents, out);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fields(settings) {
|
|
65
|
+
return settings.length
|
|
66
|
+
? settings.map((setting) => ` ${literal(setting.key)}: ${valueType(setting)};`).join('\n')
|
|
67
|
+
: '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function generateAccessModule(manifest) {
|
|
71
|
+
const settings = flattenSettings(manifest.settings);
|
|
72
|
+
const playerSettings = settings.filter((setting) => setting.scope === 'player');
|
|
73
|
+
const sessionSettings = settings.filter((setting) => setting.scope !== 'player');
|
|
74
|
+
const permissions = (manifest.permissions ?? []).filter((item) => item && typeof item.key === 'string');
|
|
75
|
+
const commands = (manifest.commands ?? []).filter((item) => item && typeof item.key === 'string');
|
|
76
|
+
|
|
77
|
+
const permissionDefaults = permissions.map((item) => ` ${literal(item.key)}: ${item.default === true},`).join('\n');
|
|
78
|
+
|
|
79
|
+
const commandArgs = commands.map((command) => {
|
|
80
|
+
const args = (command.args ?? [])
|
|
81
|
+
.filter((arg) => arg && typeof arg.name === 'string')
|
|
82
|
+
.map((arg) => {
|
|
83
|
+
const type = arg.type === 'int' || arg.type === 'duration' || arg.type === 'player'
|
|
84
|
+
? 'number'
|
|
85
|
+
: arg.type === 'enum' ? union(arg.variants, 'string') : 'string';
|
|
86
|
+
return `${literal(arg.name)}${arg.required ? '' : '?'}: ${type}`;
|
|
87
|
+
});
|
|
88
|
+
return ` ${literal(command.key)}: { ${args.join('; ')}${args.length ? ';' : ''} };`;
|
|
89
|
+
}).join('\n');
|
|
90
|
+
|
|
91
|
+
return `${header}import { createAccess, createCommands } from 'v8_scripting/access';
|
|
92
|
+
import type { AudienceValue, CommandContext, CommandResult } from 'v8_scripting/access';
|
|
93
|
+
|
|
94
|
+
/** Разрешения мода (permissions[]) */
|
|
95
|
+
export type PermissionKey = ${union(permissions.map((item) => item.key), 'never')};
|
|
96
|
+
|
|
97
|
+
/** Персональные настройки (scope: "player"): у групп и игроков могут быть свои значения */
|
|
98
|
+
export interface PlayerValues {
|
|
99
|
+
${fields(playerSettings)}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Настройки мода на сервер (scope: "session") — одно значение на всех */
|
|
103
|
+
export interface SessionSettings {
|
|
104
|
+
${fields(sessionSettings)}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Команды мода (commands[]) */
|
|
108
|
+
export type CommandKey = ${union(commands.map((item) => item.key), 'never')};
|
|
109
|
+
|
|
110
|
+
/** Аргументы команд — уже разобранные платформой: player — AccountID, duration — секунды */
|
|
111
|
+
export interface CommandArgs {
|
|
112
|
+
${commandArgs}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const access = createAccess<PermissionKey, PlayerValues, SessionSettings>({
|
|
116
|
+
permissionDefaults: {
|
|
117
|
+
${permissionDefaults}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const commandsRaw = createCommands<CommandKey>();
|
|
122
|
+
|
|
123
|
+
/** Обработчик команды с типизированными аргументами */
|
|
124
|
+
export const commands = {
|
|
125
|
+
on<K extends CommandKey>(key: K, handler: (context: CommandContext, args: CommandArgs[K]) => CommandResult): () => void {
|
|
126
|
+
return commandsRaw.on(key, handler as (context: CommandContext, args: Record<string, string | number>) => CommandResult);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Путь сгенерированного модуля — рядом с entry */
|
|
133
|
+
export function generatedPath(manifest, dir = process.cwd()) {
|
|
134
|
+
return join(dir, dirname(manifest.entry), GENERATED_FILE);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Написать модуль типов для мода apiVersion 2. Переписывает только при изменении — иначе
|
|
139
|
+
* вотчеры сборки крутились бы по кругу. false — мод apiVersion 1, типов нет
|
|
140
|
+
*/
|
|
141
|
+
export async function ensureGenerated(manifest, dir = process.cwd()) {
|
|
142
|
+
if (!(Number(manifest.apiVersion) >= 2))
|
|
143
|
+
return false;
|
|
144
|
+
|
|
145
|
+
const path = generatedPath(manifest, dir);
|
|
146
|
+
const source = generateAccessModule(manifest);
|
|
147
|
+
const current = existsSync(path) ? await readFile(path, 'utf-8') : '';
|
|
148
|
+
if (current !== source) {
|
|
149
|
+
await writeFile(path, source);
|
|
150
|
+
console.log(`ok: ${path} — типы ключей из mod.json`);
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Проверка типов проекта. Для модов apiVersion 2 обязательна: ради неё и генерируются ключи.
|
|
157
|
+
* Нужен typescript в проекте (он есть в шаблоне init)
|
|
158
|
+
*/
|
|
159
|
+
export function typecheck(dir = process.cwd()) {
|
|
160
|
+
const tsc = join(dir, 'node_modules', '.bin', process.platform === 'win32' ? 'tsc.cmd' : 'tsc');
|
|
161
|
+
if (!existsSync(tsc))
|
|
162
|
+
throw new Error('typescript is not installed in the mod — run `npm i -D typescript` (apiVersion 2 mods are typechecked on build)');
|
|
163
|
+
if (!existsSync(join(dir, 'tsconfig.json')))
|
|
164
|
+
throw new Error('tsconfig.json not found — apiVersion 2 mods are typechecked on build (see the init template)');
|
|
165
|
+
|
|
166
|
+
const result = spawnSync(tsc, ['--noEmit', '-p', 'tsconfig.json'], { cwd: dir, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
167
|
+
if (result.status !== 0)
|
|
168
|
+
throw new Error('typecheck failed — fix the errors above (keys come from mod.json, see src/' + GENERATED_FILE + ')');
|
|
169
|
+
console.log('ok: typecheck');
|
|
170
|
+
}
|