@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.
- package/config.d.ts +3 -1
- package/dist/cli.js +194 -14
- 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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
158
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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