@s2script/sdk 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.
Files changed (3) hide show
  1. package/config.d.ts +3 -1
  2. package/dist/cli.js +194 -14
  3. package/package.json +1 -1
package/config.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /** @s2script/config — typed access to the plugin's materialized config. NO runtime code. */
2
- export type Config = Record<string, string | number | boolean>;
2
+ /** A materialized config value: a scalar, or a nested section object of further values. */
3
+ export type ConfigValue = string | number | boolean | { [k: string]: ConfigValue };
4
+ export type Config = Record<string, ConfigValue>;
3
5
  export declare const config: {
4
6
  getString(key: string): string;
5
7
  getInt(key: string): number;
package/dist/cli.js CHANGED
@@ -89,6 +89,9 @@ function typecheckPlugin(pluginDir, opts) {
89
89
  const options = {
90
90
  strict: true,
91
91
  noEmit: true,
92
+ // Accept explicit `.ts` import extensions (node type-stripping requires them for source-to-source
93
+ // imports; esbuild strips them at bundle time). Backward-compatible — extensionless imports still resolve.
94
+ allowImportingTsExtensions: true,
92
95
  moduleResolution: ts.ModuleResolutionKind.Bundler,
93
96
  module: ts.ModuleKind.ESNext,
94
97
  target: ts.ScriptTarget.ES2020,
@@ -139,25 +142,100 @@ function formatDiagnostics(diags) {
139
142
  }
140
143
 
141
144
  // src/config-validate.ts
145
+ function isDeclType(t) {
146
+ return t === "string" || t === "int" || t === "float" || t === "bool";
147
+ }
148
+ function matchesType(v, t) {
149
+ switch (t) {
150
+ case "string":
151
+ return typeof v === "string";
152
+ case "int":
153
+ return typeof v === "number" && Number.isInteger(v);
154
+ case "float":
155
+ return typeof v === "number";
156
+ case "bool":
157
+ return typeof v === "boolean";
158
+ }
159
+ }
142
160
  function validateConfigBlock(config) {
143
161
  const errs = [];
144
162
  if (config == null) return errs;
145
- if (typeof config !== "object" || Array.isArray(config)) return ["s2script.config must be an object"];
146
- for (const [key, raw] of Object.entries(config)) {
147
- if (typeof raw !== "object" || raw === null) {
148
- errs.push(`config '${key}': must be { type, default }`);
163
+ validateEntries(config, "", errs);
164
+ return errs;
165
+ }
166
+ function validateEntries(node, prefix, errs) {
167
+ if (typeof node !== "object" || node === null || Array.isArray(node)) {
168
+ errs.push(`${prefix ? `config '${prefix}'` : "s2script.config"} must be an object`);
169
+ return;
170
+ }
171
+ for (const [key, raw] of Object.entries(node)) {
172
+ const full = prefix ? `${prefix}.${key}` : key;
173
+ if (key.includes(".")) {
174
+ errs.push(`config '${full}': key must not contain '.' (dotted names are reserved for section access)`);
149
175
  continue;
150
176
  }
151
- const decl = raw;
152
- const t = decl.type, d = decl.default;
153
- if (t !== "string" && t !== "int" && t !== "float" && t !== "bool") {
154
- errs.push(`config '${key}': unknown type ${JSON.stringify(t)} (want string|int|float|bool)`);
177
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
178
+ errs.push(`config '${full}': must be a declaration { type, default } or a section object`);
155
179
  continue;
156
180
  }
157
- const ok = t === "string" && typeof d === "string" || t === "int" && typeof d === "number" && Number.isInteger(d) || t === "float" && typeof d === "number" || t === "bool" && typeof d === "boolean";
158
- if (!ok) errs.push(`config '${key}': default ${JSON.stringify(d)} does not match type '${t}'`);
181
+ const obj = raw;
182
+ if (typeof obj.type === "string") {
183
+ validateDecl(full, obj, obj.type, errs);
184
+ } else {
185
+ validateEntries(obj, full, errs);
186
+ }
159
187
  }
160
- return errs;
188
+ }
189
+ function validateDecl(full, decl, typeStr, errs) {
190
+ if (!isDeclType(typeStr)) {
191
+ errs.push(`config '${full}': unknown type ${JSON.stringify(typeStr)} (want string|int|float|bool)`);
192
+ return;
193
+ }
194
+ const t = typeStr;
195
+ if (!("default" in decl)) {
196
+ errs.push(`config '${full}': missing 'default'`);
197
+ } else if (!matchesType(decl.default, t)) {
198
+ errs.push(`config '${full}': default ${JSON.stringify(decl.default)} does not match type '${t}'`);
199
+ }
200
+ const hasMin = decl.min !== void 0;
201
+ const hasMax = decl.max !== void 0;
202
+ const hasEnum = decl.enum !== void 0;
203
+ if (hasEnum && (hasMin || hasMax)) {
204
+ errs.push(`config '${full}': 'enum' is mutually exclusive with 'min'/'max'`);
205
+ }
206
+ if (hasMin || hasMax) {
207
+ if (t !== "int" && t !== "float") {
208
+ errs.push(`config '${full}': 'min'/'max' apply to int|float only (type '${t}')`);
209
+ }
210
+ if (hasMin && typeof decl.min !== "number") errs.push(`config '${full}': 'min' must be a number`);
211
+ if (hasMax && typeof decl.max !== "number") errs.push(`config '${full}': 'max' must be a number`);
212
+ if (typeof decl.min === "number" && typeof decl.max === "number" && decl.min > decl.max) {
213
+ errs.push(`config '${full}': 'min' (${decl.min}) exceeds 'max' (${decl.max})`);
214
+ }
215
+ if (typeof decl.default === "number") {
216
+ if (typeof decl.min === "number" && decl.default < decl.min) errs.push(`config '${full}': default ${decl.default} is below min ${decl.min}`);
217
+ if (typeof decl.max === "number" && decl.default > decl.max) errs.push(`config '${full}': default ${decl.default} is above max ${decl.max}`);
218
+ }
219
+ }
220
+ if (hasEnum) {
221
+ if (t !== "string" && t !== "int") {
222
+ errs.push(`config '${full}': 'enum' applies to string|int only (type '${t}')`);
223
+ }
224
+ if (!Array.isArray(decl.enum) || decl.enum.length === 0) {
225
+ errs.push(`config '${full}': 'enum' must be a non-empty array`);
226
+ } else {
227
+ for (const e of decl.enum) {
228
+ if (!matchesType(e, t)) errs.push(`config '${full}': enum value ${JSON.stringify(e)} does not match type '${t}'`);
229
+ }
230
+ if ("default" in decl && matchesType(decl.default, t) && !decl.enum.some((e) => e === decl.default)) {
231
+ errs.push(`config '${full}': default ${JSON.stringify(decl.default)} is not one of enum`);
232
+ }
233
+ }
234
+ }
235
+ if (decl.sensitive !== void 0 && typeof decl.sensitive !== "boolean") errs.push(`config '${full}': 'sensitive' must be a boolean`);
236
+ if (decl.group !== void 0 && typeof decl.group !== "string") errs.push(`config '${full}': 'group' must be a string`);
237
+ if (decl.label !== void 0 && typeof decl.label !== "string") errs.push(`config '${full}': 'label' must be a string`);
238
+ if (decl.description !== void 0 && typeof decl.description !== "string") errs.push(`config '${full}': 'description' must be a string`);
161
239
  }
162
240
 
163
241
  // src/publish-gate.ts
@@ -1176,13 +1254,85 @@ async function createPlugin(opts = {}) {
1176
1254
  };
1177
1255
  }
1178
1256
 
1257
+ // src/config/gen.ts
1258
+ import AdmZip2 from "adm-zip";
1259
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync3 } from "node:fs";
1260
+ import { join as join9 } from "node:path";
1261
+
1262
+ // src/config/config-path.ts
1263
+ function configFileName(id) {
1264
+ return id.replace(/[^A-Za-z0-9._-]/g, "_") + ".json";
1265
+ }
1266
+
1267
+ // src/config/gen.ts
1268
+ function isDecl(v) {
1269
+ return typeof v.type === "string";
1270
+ }
1271
+ function generateDefaultJsonc(config) {
1272
+ return "{\n" + genEntries(config, 1) + "}\n";
1273
+ }
1274
+ function genEntries(entries, indent) {
1275
+ const pad = " ".repeat(indent);
1276
+ const keys = Object.keys(entries).filter((k) => !k.includes(".")).sort();
1277
+ let out = "";
1278
+ keys.forEach((key, i) => {
1279
+ const comma = i + 1 < keys.length ? "," : "";
1280
+ const raw = entries[key];
1281
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
1282
+ const obj = raw;
1283
+ if (isDecl(obj)) {
1284
+ const desc = typeof obj.description === "string" ? obj.description : "";
1285
+ out += `${pad}// ${String(obj.type)}${desc ? ` \u2014 ${desc}` : ""}
1286
+ `;
1287
+ out += `${pad}${JSON.stringify(key)}: ${JSON.stringify(obj.default)}${comma}
1288
+ `;
1289
+ } else {
1290
+ out += `${pad}${JSON.stringify(key)}: {
1291
+ `;
1292
+ out += genEntries(obj, indent + 1);
1293
+ out += `${pad}}${comma}
1294
+ `;
1295
+ }
1296
+ });
1297
+ return out;
1298
+ }
1299
+ function genConfigFromManifest(manifest, outDir) {
1300
+ const config = manifest.config;
1301
+ if (config == null || typeof config !== "object" || Array.isArray(config) || Object.keys(config).length === 0) {
1302
+ return null;
1303
+ }
1304
+ const id = typeof manifest.id === "string" ? manifest.id : "";
1305
+ if (!id) throw new Error("config gen: manifest declares config but has no id");
1306
+ mkdirSync3(outDir, { recursive: true });
1307
+ const path = join9(outDir, configFileName(id));
1308
+ writeFileSync6(path, generateDefaultJsonc(config));
1309
+ return path;
1310
+ }
1311
+ function genConfigForS2sp(s2spPath, outDir) {
1312
+ const zip = new AdmZip2(s2spPath);
1313
+ const entry = zip.getEntry("manifest.json");
1314
+ if (!entry) throw new Error(`config gen: ${s2spPath} has no manifest.json`);
1315
+ const manifest = JSON.parse(entry.getData().toString("utf8"));
1316
+ return genConfigFromManifest(manifest, outDir);
1317
+ }
1318
+ function runConfigGen(s2sps, outDir) {
1319
+ const written = [];
1320
+ const skipped = [];
1321
+ for (const p of s2sps) {
1322
+ const r = genConfigForS2sp(p, outDir);
1323
+ if (r) written.push(r);
1324
+ else skipped.push(p);
1325
+ }
1326
+ return { written, skipped };
1327
+ }
1328
+
1179
1329
  // src/cli.ts
1180
1330
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1181
- import { dirname as dirname3, join as join9, resolve as resolve6 } from "node:path";
1331
+ import { dirname as dirname3, join as join10, resolve as resolve6 } from "node:path";
1182
1332
  var argv = process.argv.slice(2);
1183
1333
  var command = argv[0];
1184
1334
  function repoRootFromCli() {
1185
- return join9(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1335
+ return join10(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1186
1336
  }
1187
1337
  function parseFlag(args, name) {
1188
1338
  const eq = args.find((a) => a.startsWith(`${name}=`));
@@ -1194,6 +1344,20 @@ function parseFlag(args, name) {
1194
1344
  function hasFlag(args, name) {
1195
1345
  return args.includes(name);
1196
1346
  }
1347
+ function positionals(args, flagsWithValue) {
1348
+ const out = [];
1349
+ for (let i = 0; i < args.length; i++) {
1350
+ const a = args[i];
1351
+ if (a.startsWith("--")) {
1352
+ const name = a.split("=")[0];
1353
+ if (flagsWithValue.includes(name) && !a.includes("=")) i++;
1354
+ continue;
1355
+ }
1356
+ if (a.startsWith("-")) continue;
1357
+ out.push(a);
1358
+ }
1359
+ return out;
1360
+ }
1197
1361
  if (command === "gen-schema") {
1198
1362
  const repoRoot = repoRootFromCli();
1199
1363
  const check = argv[1] === "--check";
@@ -1257,6 +1421,22 @@ if (command === "gen-schema") {
1257
1421
  console.error(String(e instanceof Error ? e.message : e));
1258
1422
  process.exit(1);
1259
1423
  }
1424
+ } else if (command === "config" && argv[1] === "gen") {
1425
+ const args = argv.slice(2);
1426
+ const outDir = parseFlag(args, "--out") ?? process.cwd();
1427
+ const s2sps = positionals(args, ["--out"]);
1428
+ if (s2sps.length === 0) {
1429
+ console.error("Usage: s2s config gen <plugin.s2sp...> --out <dir>");
1430
+ process.exit(1);
1431
+ }
1432
+ try {
1433
+ const { written, skipped } = runConfigGen(s2sps, outDir);
1434
+ for (const w of written) console.log(`config gen: wrote ${w}`);
1435
+ for (const s of skipped) console.log(`config gen: ${s} declares no config \u2014 skipped`);
1436
+ } catch (e) {
1437
+ console.error(String(e instanceof Error ? e.message : e));
1438
+ process.exit(1);
1439
+ }
1260
1440
  } else if (command === "build" && argv[1]) {
1261
1441
  const args = argv.slice(1);
1262
1442
  const dir = args.find((a) => !a.startsWith("-"));
@@ -1274,7 +1454,7 @@ if (command === "gen-schema") {
1274
1454
  }
1275
1455
  } else {
1276
1456
  console.error(
1277
- "Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s build <dir> [--packages-dir <path>]\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]"
1457
+ "Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s build <dir> [--packages-dir <path>]\n s2s config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]"
1278
1458
  );
1279
1459
  process.exit(1);
1280
1460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2script/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "s2script SDK — the builtin capability types (imported as @s2script/sdk/entity, …) plus the `s2s` CLI for building Source 2 / CS2 plugins.",
5
5
  "type": "module",
6
6
  "bin": {