v8scli 0.3.0 → 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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v8scli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for building and publishing V8 point_script mods (companion of v8_scripting)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,
@@ -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,6 +42,10 @@ 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);
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
+ }
@@ -7,8 +7,8 @@
7
7
  "upload": "v8scli upload"
8
8
  },
9
9
  "devDependencies": {
10
- "v8_scripting": "^0.3.0",
11
- "v8scli": "^0.2.0",
10
+ "v8_scripting": "^0.27.0",
11
+ "v8scli": "^0.4.0",
12
12
  "typescript": "^5.9.2"
13
13
  }
14
14
  }