@s2script/sdk 0.3.0 → 0.5.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/chat.d.ts +0 -7
- package/clients.d.ts +0 -18
- package/commands.d.ts +1 -4
- package/config.d.ts +3 -3
- package/cookies.d.ts +0 -3
- package/damage.d.ts +0 -5
- package/dist/cli.js +494 -135
- package/entity.d.ts +21 -36
- package/events.d.ts +0 -13
- package/interfaces.d.ts +1 -24
- package/package.json +7 -5
- package/plugin.d.ts +112 -0
- package/plugins.d.ts +11 -1
- package/server.d.ts +0 -7
- package/sound.d.ts +0 -4
- package/topmenu.d.ts +0 -4
- package/usercmd.d.ts +5 -18
- package/frame.d.ts +0 -13
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
import * as esbuild from "esbuild";
|
|
5
5
|
import AdmZip from "adm-zip";
|
|
6
6
|
import { readFileSync as readFileSync4, mkdirSync } from "node:fs";
|
|
7
|
-
import { resolve as
|
|
7
|
+
import { resolve as resolve5, join as join6 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/typecheck/typecheck.ts
|
|
10
10
|
import ts from "typescript";
|
|
11
|
-
import { existsSync as
|
|
12
|
-
import { join as
|
|
11
|
+
import { existsSync as existsSync3, readdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
|
|
12
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
|
|
15
15
|
// src/packages-resolve.ts
|
|
@@ -55,11 +55,47 @@ function resolvePackagesDir(opts) {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// src/tsconfig-shared.ts
|
|
59
|
+
var sharedCompilerOptionsJson = {
|
|
60
|
+
strict: true,
|
|
61
|
+
noEmit: true,
|
|
62
|
+
moduleResolution: "bundler",
|
|
63
|
+
module: "ESNext",
|
|
64
|
+
target: "ES2020",
|
|
65
|
+
lib: ["ES2020"],
|
|
66
|
+
types: [],
|
|
67
|
+
skipLibCheck: true,
|
|
68
|
+
allowImportingTsExtensions: true
|
|
69
|
+
};
|
|
70
|
+
function sharedProgramOptions(tsMod) {
|
|
71
|
+
return {
|
|
72
|
+
strict: sharedCompilerOptionsJson.strict,
|
|
73
|
+
noEmit: sharedCompilerOptionsJson.noEmit,
|
|
74
|
+
allowImportingTsExtensions: sharedCompilerOptionsJson.allowImportingTsExtensions,
|
|
75
|
+
moduleResolution: tsMod.ModuleResolutionKind.Bundler,
|
|
76
|
+
module: tsMod.ModuleKind.ESNext,
|
|
77
|
+
target: tsMod.ScriptTarget.ES2020,
|
|
78
|
+
lib: sharedCompilerOptionsJson.lib.map((l) => `lib.${l.toLowerCase()}.d.ts`),
|
|
79
|
+
types: [...sharedCompilerOptionsJson.types],
|
|
80
|
+
skipLibCheck: sharedCompilerOptionsJson.skipLibCheck
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/contracts.ts
|
|
85
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
86
|
+
import { join as join2 } from "node:path";
|
|
87
|
+
function localContractPath(pluginDir, dep) {
|
|
88
|
+
const segs = dep.split("/");
|
|
89
|
+
if (segs.some((s) => s === "" || s === "." || s === "..")) return null;
|
|
90
|
+
const p = join2(pluginDir, ".s2script", "types", ...segs, "index.d.ts");
|
|
91
|
+
return existsSync2(p) ? p : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
// src/typecheck/typecheck.ts
|
|
59
95
|
function localDeclarationFiles(pluginDir) {
|
|
60
|
-
const srcDir =
|
|
61
|
-
if (!
|
|
62
|
-
return readdirSync(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) =>
|
|
96
|
+
const srcDir = join3(pluginDir, "src");
|
|
97
|
+
if (!existsSync3(srcDir)) return [];
|
|
98
|
+
return readdirSync(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join3(srcDir, f));
|
|
63
99
|
}
|
|
64
100
|
function declaredModules(dtsFiles) {
|
|
65
101
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -72,28 +108,33 @@ function declaredModules(dtsFiles) {
|
|
|
72
108
|
function typecheckPlugin(pluginDir, opts) {
|
|
73
109
|
const absDir = resolve2(pluginDir);
|
|
74
110
|
const packagesDir = opts?.packagesDir ? resolve2(opts.packagesDir) : resolvePackagesDir({ pluginDir: absDir });
|
|
75
|
-
const pkg = JSON.parse(readFileSync(
|
|
111
|
+
const pkg = JSON.parse(readFileSync(join3(absDir, "package.json"), "utf8"));
|
|
76
112
|
const s2 = pkg.s2script ?? {};
|
|
77
113
|
const entryRel = s2.main ?? pkg.main;
|
|
78
|
-
if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${
|
|
114
|
+
if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join3(absDir, "package.json")}`);
|
|
79
115
|
const entry = resolve2(absDir, entryRel);
|
|
80
116
|
const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
|
|
81
117
|
const localDts = localDeclarationFiles(absDir);
|
|
82
118
|
const locallyDeclared = declaredModules(localDts);
|
|
119
|
+
const allDeclaredDeps = [
|
|
120
|
+
...Object.keys(s2.pluginDependencies ?? {}),
|
|
121
|
+
...Object.keys(s2.optionalPluginDependencies ?? {})
|
|
122
|
+
];
|
|
123
|
+
const contractPaths = {};
|
|
124
|
+
for (const d of allDeclaredDeps) {
|
|
125
|
+
const p = localContractPath(absDir, d);
|
|
126
|
+
if (p !== null) contractPaths[d] = [p];
|
|
127
|
+
}
|
|
83
128
|
const deps = [
|
|
84
129
|
...Object.keys(s2.pluginDependencies ?? {}),
|
|
85
130
|
...Object.keys(s2.optionalPluginDependencies ?? {})
|
|
86
131
|
// Never stub a module the plugin declares itself — a shorthand `declare module "X";` and a
|
|
87
132
|
// full `declare module "X" { … }` for the same X collide.
|
|
88
|
-
].filter((d) => !isAlwaysResolved(d) && !locallyDeclared.has(d));
|
|
133
|
+
].filter((d) => !isAlwaysResolved(d) && !locallyDeclared.has(d) && contractPaths[d] === void 0);
|
|
89
134
|
const options = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
module: ts.ModuleKind.ESNext,
|
|
94
|
-
target: ts.ScriptTarget.ES2020,
|
|
95
|
-
lib: ["lib.es2020.d.ts"],
|
|
96
|
-
types: [],
|
|
135
|
+
// Accept explicit `.ts` import extensions (node type-stripping requires them for source-to-source
|
|
136
|
+
// imports; esbuild strips them at bundle time). Backward-compatible — extensionless imports still resolve.
|
|
137
|
+
...sharedProgramOptions(ts),
|
|
97
138
|
baseUrl: packagesDir,
|
|
98
139
|
paths: {
|
|
99
140
|
// Builtins are `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback
|
|
@@ -101,15 +142,15 @@ function typecheckPlugin(pluginDir, opts) {
|
|
|
101
142
|
// dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
|
|
102
143
|
// sdk imports.
|
|
103
144
|
"@s2script/sdk/*": ["sdk/*.d.ts"],
|
|
104
|
-
"@s2script/*": ["*/index.d.ts"]
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
"@s2script/*": ["*/index.d.ts"],
|
|
146
|
+
...contractPaths
|
|
147
|
+
}
|
|
107
148
|
};
|
|
108
|
-
const rootNames = [entry,
|
|
109
|
-
const tmp = mkdtempSync(
|
|
149
|
+
const rootNames = [entry, join3(packagesDir, "sdk", "globals.d.ts"), ...localDts];
|
|
150
|
+
const tmp = mkdtempSync(join3(tmpdir(), "s2tc-"));
|
|
110
151
|
try {
|
|
111
152
|
if (deps.length) {
|
|
112
|
-
const stub =
|
|
153
|
+
const stub = join3(tmp, "ambient.d.ts");
|
|
113
154
|
writeFileSync(stub, deps.map((d) => `declare module ${JSON.stringify(d)};`).join("\n") + "\n");
|
|
114
155
|
rootNames.push(stub);
|
|
115
156
|
}
|
|
@@ -129,7 +170,7 @@ function typecheckPlugin(pluginDir, opts) {
|
|
|
129
170
|
}
|
|
130
171
|
return { file, line, col, code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, "\n") };
|
|
131
172
|
});
|
|
132
|
-
return { ok: out.length === 0, diagnostics: out };
|
|
173
|
+
return { ok: out.length === 0, diagnostics: out, program };
|
|
133
174
|
} finally {
|
|
134
175
|
rmSync(tmp, { recursive: true, force: true });
|
|
135
176
|
}
|
|
@@ -139,30 +180,105 @@ function formatDiagnostics(diags) {
|
|
|
139
180
|
}
|
|
140
181
|
|
|
141
182
|
// src/config-validate.ts
|
|
183
|
+
function isDeclType(t) {
|
|
184
|
+
return t === "string" || t === "int" || t === "float" || t === "bool";
|
|
185
|
+
}
|
|
186
|
+
function matchesType(v, t) {
|
|
187
|
+
switch (t) {
|
|
188
|
+
case "string":
|
|
189
|
+
return typeof v === "string";
|
|
190
|
+
case "int":
|
|
191
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
192
|
+
case "float":
|
|
193
|
+
return typeof v === "number";
|
|
194
|
+
case "bool":
|
|
195
|
+
return typeof v === "boolean";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
142
198
|
function validateConfigBlock(config) {
|
|
143
199
|
const errs = [];
|
|
144
200
|
if (config == null) return errs;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
201
|
+
validateEntries(config, "", errs);
|
|
202
|
+
return errs;
|
|
203
|
+
}
|
|
204
|
+
function validateEntries(node, prefix, errs) {
|
|
205
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
206
|
+
errs.push(`${prefix ? `config '${prefix}'` : "s2script.config"} must be an object`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const [key, raw] of Object.entries(node)) {
|
|
210
|
+
const full = prefix ? `${prefix}.${key}` : key;
|
|
211
|
+
if (key.includes(".")) {
|
|
212
|
+
errs.push(`config '${full}': key must not contain '.' (dotted names are reserved for section access)`);
|
|
149
213
|
continue;
|
|
150
214
|
}
|
|
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)`);
|
|
215
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
216
|
+
errs.push(`config '${full}': must be a declaration { type, default } or a section object`);
|
|
155
217
|
continue;
|
|
156
218
|
}
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
219
|
+
const obj = raw;
|
|
220
|
+
if (typeof obj.type === "string") {
|
|
221
|
+
validateDecl(full, obj, obj.type, errs);
|
|
222
|
+
} else {
|
|
223
|
+
validateEntries(obj, full, errs);
|
|
224
|
+
}
|
|
159
225
|
}
|
|
160
|
-
|
|
226
|
+
}
|
|
227
|
+
function validateDecl(full, decl, typeStr, errs) {
|
|
228
|
+
if (!isDeclType(typeStr)) {
|
|
229
|
+
errs.push(`config '${full}': unknown type ${JSON.stringify(typeStr)} (want string|int|float|bool)`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const t = typeStr;
|
|
233
|
+
if (!("default" in decl)) {
|
|
234
|
+
errs.push(`config '${full}': missing 'default'`);
|
|
235
|
+
} else if (!matchesType(decl.default, t)) {
|
|
236
|
+
errs.push(`config '${full}': default ${JSON.stringify(decl.default)} does not match type '${t}'`);
|
|
237
|
+
}
|
|
238
|
+
const hasMin = decl.min !== void 0;
|
|
239
|
+
const hasMax = decl.max !== void 0;
|
|
240
|
+
const hasEnum = decl.enum !== void 0;
|
|
241
|
+
if (hasEnum && (hasMin || hasMax)) {
|
|
242
|
+
errs.push(`config '${full}': 'enum' is mutually exclusive with 'min'/'max'`);
|
|
243
|
+
}
|
|
244
|
+
if (hasMin || hasMax) {
|
|
245
|
+
if (t !== "int" && t !== "float") {
|
|
246
|
+
errs.push(`config '${full}': 'min'/'max' apply to int|float only (type '${t}')`);
|
|
247
|
+
}
|
|
248
|
+
if (hasMin && typeof decl.min !== "number") errs.push(`config '${full}': 'min' must be a number`);
|
|
249
|
+
if (hasMax && typeof decl.max !== "number") errs.push(`config '${full}': 'max' must be a number`);
|
|
250
|
+
if (typeof decl.min === "number" && typeof decl.max === "number" && decl.min > decl.max) {
|
|
251
|
+
errs.push(`config '${full}': 'min' (${decl.min}) exceeds 'max' (${decl.max})`);
|
|
252
|
+
}
|
|
253
|
+
if (typeof decl.default === "number") {
|
|
254
|
+
if (typeof decl.min === "number" && decl.default < decl.min) errs.push(`config '${full}': default ${decl.default} is below min ${decl.min}`);
|
|
255
|
+
if (typeof decl.max === "number" && decl.default > decl.max) errs.push(`config '${full}': default ${decl.default} is above max ${decl.max}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (hasEnum) {
|
|
259
|
+
if (t !== "string" && t !== "int") {
|
|
260
|
+
errs.push(`config '${full}': 'enum' applies to string|int only (type '${t}')`);
|
|
261
|
+
}
|
|
262
|
+
if (!Array.isArray(decl.enum) || decl.enum.length === 0) {
|
|
263
|
+
errs.push(`config '${full}': 'enum' must be a non-empty array`);
|
|
264
|
+
} else {
|
|
265
|
+
for (const e of decl.enum) {
|
|
266
|
+
if (!matchesType(e, t)) errs.push(`config '${full}': enum value ${JSON.stringify(e)} does not match type '${t}'`);
|
|
267
|
+
}
|
|
268
|
+
if ("default" in decl && matchesType(decl.default, t) && !decl.enum.some((e) => e === decl.default)) {
|
|
269
|
+
errs.push(`config '${full}': default ${JSON.stringify(decl.default)} is not one of enum`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (decl.sensitive !== void 0 && typeof decl.sensitive !== "boolean") errs.push(`config '${full}': 'sensitive' must be a boolean`);
|
|
274
|
+
if (decl.group !== void 0 && typeof decl.group !== "string") errs.push(`config '${full}': 'group' must be a string`);
|
|
275
|
+
if (decl.label !== void 0 && typeof decl.label !== "string") errs.push(`config '${full}': 'label' must be a string`);
|
|
276
|
+
if (decl.description !== void 0 && typeof decl.description !== "string") errs.push(`config '${full}': 'description' must be a string`);
|
|
161
277
|
}
|
|
162
278
|
|
|
163
279
|
// src/publish-gate.ts
|
|
164
|
-
import { existsSync as
|
|
165
|
-
import { join as
|
|
280
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
281
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
166
282
|
function hasPublishes(publishes) {
|
|
167
283
|
if (publishes == null) return false;
|
|
168
284
|
if (typeof publishes === "string") return publishes.trim().length > 0;
|
|
@@ -188,7 +304,7 @@ function assertPublishesTypes(pkg, pluginDir) {
|
|
|
188
304
|
};
|
|
189
305
|
}
|
|
190
306
|
const typesPath = resolve3(pluginDir, typesRel);
|
|
191
|
-
if (!
|
|
307
|
+
if (!existsSync4(typesPath)) {
|
|
192
308
|
return { ok: false, error: `types file not found: ${typesRel}` };
|
|
193
309
|
}
|
|
194
310
|
const st = statSync(typesPath);
|
|
@@ -263,32 +379,160 @@ function derivePublishes(authored, pkgName, pkgVersion, typesPath) {
|
|
|
263
379
|
return out;
|
|
264
380
|
}
|
|
265
381
|
|
|
382
|
+
// src/api-version.ts
|
|
383
|
+
var HOST_API_VERSION_MAJOR = 2;
|
|
384
|
+
var STAMPED_API_VERSION = `${HOST_API_VERSION_MAJOR}.x`;
|
|
385
|
+
|
|
386
|
+
// src/publish-scan.ts
|
|
387
|
+
import ts2 from "typescript";
|
|
388
|
+
function isPluginContext(type) {
|
|
389
|
+
const sym = type.getSymbol() ?? type.aliasSymbol;
|
|
390
|
+
return sym?.getName() === "PluginContext";
|
|
391
|
+
}
|
|
392
|
+
function scanPluginProgram(program, pluginDir) {
|
|
393
|
+
const checker = program.getTypeChecker();
|
|
394
|
+
const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
|
|
395
|
+
const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
|
|
396
|
+
for (const sf of program.getSourceFiles()) {
|
|
397
|
+
if (sf.isDeclarationFile) continue;
|
|
398
|
+
if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
|
|
399
|
+
const visit = (node) => {
|
|
400
|
+
if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
|
|
401
|
+
const method = node.expression.name.text;
|
|
402
|
+
if (method === "publish" || method === "use" || method === "tryUse") {
|
|
403
|
+
const recv = checker.getTypeAtLocation(node.expression.expression);
|
|
404
|
+
if (isPluginContext(recv)) {
|
|
405
|
+
const arg0 = node.arguments[0];
|
|
406
|
+
if (method === "publish") {
|
|
407
|
+
if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
|
|
408
|
+
out.publishNames.push(arg0.text);
|
|
409
|
+
} else {
|
|
410
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
411
|
+
out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
|
|
412
|
+
}
|
|
413
|
+
} else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
|
|
414
|
+
out.useNames.push(arg0.text);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
ts2.forEachChild(node, visit);
|
|
420
|
+
};
|
|
421
|
+
visit(sf);
|
|
422
|
+
}
|
|
423
|
+
out.publishNames = [...new Set(out.publishNames)];
|
|
424
|
+
out.useNames = [...new Set(out.useNames)];
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/lint/lint.ts
|
|
429
|
+
import { ESLint } from "eslint";
|
|
430
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
431
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
432
|
+
import s2lint from "@s2script/eslint-plugin";
|
|
433
|
+
var CONFIG_FILES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"];
|
|
434
|
+
async function lintPlugin(pluginDir, program) {
|
|
435
|
+
const absDir = resolve4(pluginDir);
|
|
436
|
+
const hasOwnConfig = CONFIG_FILES.some((f) => existsSync5(join5(absDir, f)));
|
|
437
|
+
const eslint = hasOwnConfig ? new ESLint({ cwd: absDir, errorOnUnmatchedPattern: false }) : new ESLint({
|
|
438
|
+
cwd: absDir,
|
|
439
|
+
overrideConfigFile: true,
|
|
440
|
+
overrideConfig: s2lint.configs.build([program]),
|
|
441
|
+
errorOnUnmatchedPattern: false
|
|
442
|
+
});
|
|
443
|
+
const dirPrefix = absDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
|
|
444
|
+
const targets = hasOwnConfig ? ["**/*.ts"] : program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)).map((sf) => sf.fileName);
|
|
445
|
+
if (targets.length === 0) return { ok: true, output: "", errorCount: 0 };
|
|
446
|
+
const results = await eslint.lintFiles(targets);
|
|
447
|
+
const errorCount = results.reduce((n, r) => n + r.errorCount, 0);
|
|
448
|
+
const formatter = await eslint.loadFormatter("stylish");
|
|
449
|
+
const output2 = String(await formatter.format(results));
|
|
450
|
+
return { ok: errorCount === 0, output: output2, errorCount };
|
|
451
|
+
}
|
|
452
|
+
|
|
266
453
|
// src/build.ts
|
|
267
454
|
async function buildPlugin(dir, packagesDir) {
|
|
268
|
-
const absDir =
|
|
269
|
-
const pkgPath =
|
|
455
|
+
const absDir = resolve5(dir);
|
|
456
|
+
const pkgPath = join6(absDir, "package.json");
|
|
270
457
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
271
|
-
const gate = assertPublishesTypes(pkg, absDir);
|
|
272
|
-
if (!gate.ok) {
|
|
273
|
-
throw new Error(`publish gate failed: ${gate.error}`);
|
|
274
|
-
}
|
|
275
458
|
const s2 = pkg.s2script ?? {};
|
|
276
|
-
const
|
|
459
|
+
const config = s2.config ?? void 0;
|
|
460
|
+
if (config !== void 0) {
|
|
461
|
+
const cfgErrs = validateConfigBlock(config);
|
|
462
|
+
if (cfgErrs.length) throw new Error(`invalid s2script.config:
|
|
463
|
+
${cfgErrs.join("\n ")}`);
|
|
464
|
+
}
|
|
277
465
|
const tc = typecheckPlugin(absDir, packagesDir !== void 0 ? { packagesDir } : void 0);
|
|
278
466
|
if (!tc.ok) {
|
|
279
467
|
throw new Error(`typecheck failed (${tc.diagnostics.length} error(s)):
|
|
280
468
|
${formatDiagnostics(tc.diagnostics)}`);
|
|
281
469
|
}
|
|
282
|
-
const
|
|
283
|
-
const
|
|
470
|
+
const scan = scanPluginProgram(tc.program, absDir);
|
|
471
|
+
const lint = await lintPlugin(absDir, tc.program);
|
|
472
|
+
if (!lint.ok) {
|
|
473
|
+
throw new Error(`lint failed (${lint.errorCount} error(s)):
|
|
474
|
+
${lint.output}`);
|
|
475
|
+
}
|
|
476
|
+
if (lint.output.trim().length > 0) console.warn(lint.output);
|
|
477
|
+
if (scan.dynamicPublishSites.length > 0) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`ctx.publish name must be a string literal (the manifest publishes block is derived from code):
|
|
480
|
+
` + scan.dynamicPublishSites.join("\n ")
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
let effectivePublishes = s2.publishes;
|
|
484
|
+
if (!hasPublishes(effectivePublishes)) {
|
|
485
|
+
if (scan.publishNames.length === 1 && scan.publishNames[0] === pkg.name) {
|
|
486
|
+
effectivePublishes = "self";
|
|
487
|
+
} else if (scan.publishNames.length > 0) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`code publishes ${JSON.stringify(scan.publishNames)} but s2script.publishes is missing \u2014 a contract named differently from the package needs an authored entry with a concrete version`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
const authoredNames = Object.keys(expandPublishes(effectivePublishes, pkg.name, pkg.version)).sort();
|
|
494
|
+
const codeNames = [...scan.publishNames].sort();
|
|
495
|
+
if (JSON.stringify(authoredNames) !== JSON.stringify(codeNames)) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`publishes drift: package.json declares ${JSON.stringify(authoredNames)} but the code's ctx.publish calls are ${JSON.stringify(codeNames)} \u2014 fix whichever is wrong (the manifest is generated from code; the loader re-verifies at Active)`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const gate = assertPublishesTypes({ ...pkg, s2script: { ...s2, publishes: effectivePublishes } }, absDir);
|
|
502
|
+
if (!gate.ok) {
|
|
503
|
+
throw new Error(`publish gate failed: ${gate.error}`);
|
|
504
|
+
}
|
|
505
|
+
const derivedPublishes = derivePublishes(
|
|
506
|
+
effectivePublishes,
|
|
507
|
+
pkg.name,
|
|
508
|
+
pkg.version,
|
|
509
|
+
gate.typesPath
|
|
510
|
+
);
|
|
284
511
|
const pluginDependencies = s2.pluginDependencies ?? {};
|
|
285
512
|
const optionalPluginDependencies = s2.optionalPluginDependencies ?? {};
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
513
|
+
const declaredDeps = /* @__PURE__ */ new Set([
|
|
514
|
+
...Object.keys(pluginDependencies),
|
|
515
|
+
...Object.keys(optionalPluginDependencies)
|
|
516
|
+
]);
|
|
517
|
+
for (const used of scan.useNames) {
|
|
518
|
+
if (!declaredDeps.has(used)) {
|
|
519
|
+
console.warn(
|
|
520
|
+
`WARN: ctx.use/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (const dep of declaredDeps) {
|
|
525
|
+
if (!scan.useNames.includes(dep)) {
|
|
526
|
+
console.warn(`WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d`);
|
|
527
|
+
}
|
|
291
528
|
}
|
|
529
|
+
const { name, version } = pkg;
|
|
530
|
+
if (s2.apiVersion !== void 0) {
|
|
531
|
+
console.warn(
|
|
532
|
+
`WARN: ${pkgPath}: s2script.apiVersion is ignored \u2014 s2s build derives apiVersion from the SDK (stamping ${JSON.stringify(STAMPED_API_VERSION)}). Remove the field.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const apiVersion = STAMPED_API_VERSION;
|
|
292
536
|
const external = Array.from(/* @__PURE__ */ new Set([
|
|
293
537
|
"@s2script/*",
|
|
294
538
|
...Object.keys(pluginDependencies),
|
|
@@ -300,7 +544,7 @@ ${formatDiagnostics(tc.diagnostics)}`);
|
|
|
300
544
|
`buildPlugin: no entry point found in ${pkgPath} (set s2script.main or main)`
|
|
301
545
|
);
|
|
302
546
|
}
|
|
303
|
-
const entryPoint =
|
|
547
|
+
const entryPoint = join6(absDir, entryRelative);
|
|
304
548
|
const result = await esbuild.build({
|
|
305
549
|
entryPoints: [entryPoint],
|
|
306
550
|
bundle: true,
|
|
@@ -323,6 +567,15 @@ ${formatDiagnostics(tc.diagnostics)}`);
|
|
|
323
567
|
manifest.publishes = derivedPublishes;
|
|
324
568
|
}
|
|
325
569
|
if (config !== void 0) manifest.config = config;
|
|
570
|
+
const compiledAgainst = {};
|
|
571
|
+
for (const dep of [
|
|
572
|
+
...Object.keys(pluginDependencies),
|
|
573
|
+
...Object.keys(optionalPluginDependencies)
|
|
574
|
+
]) {
|
|
575
|
+
const contractPath = localContractPath(absDir, dep);
|
|
576
|
+
if (contractPath !== null) compiledAgainst[dep] = hashContract(contractPath);
|
|
577
|
+
}
|
|
578
|
+
if (Object.keys(compiledAgainst).length > 0) manifest.compiledAgainst = compiledAgainst;
|
|
326
579
|
const zip = new AdmZip();
|
|
327
580
|
zip.addFile("manifest.json", Buffer.from(JSON.stringify(manifest, null, 2)));
|
|
328
581
|
zip.addFile("plugin.js", Buffer.from(pluginJs));
|
|
@@ -334,16 +587,16 @@ ${formatDiagnostics(tc.diagnostics)}`);
|
|
|
334
587
|
}
|
|
335
588
|
}
|
|
336
589
|
const sanitizedId = name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
337
|
-
const outDir =
|
|
590
|
+
const outDir = join6(absDir, "dist");
|
|
338
591
|
mkdirSync(outDir, { recursive: true });
|
|
339
|
-
const outPath =
|
|
592
|
+
const outPath = join6(outDir, `${sanitizedId}.s2sp`);
|
|
340
593
|
zip.writeZip(outPath);
|
|
341
594
|
return outPath;
|
|
342
595
|
}
|
|
343
596
|
|
|
344
597
|
// src/schemagen/gen.ts
|
|
345
598
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
346
|
-
import { join as
|
|
599
|
+
import { join as join7 } from "node:path";
|
|
347
600
|
|
|
348
601
|
// src/schemagen/model.ts
|
|
349
602
|
var READ = {
|
|
@@ -535,21 +788,21 @@ function emitJs(model) {
|
|
|
535
788
|
for (const c of model.classes) {
|
|
536
789
|
out.push(` A[${S(c.className)}] = {`);
|
|
537
790
|
for (const f of flattenedFields(model, c.className)) {
|
|
538
|
-
const
|
|
791
|
+
const resolve8 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
539
792
|
let entry;
|
|
540
793
|
if (f.accessorKind === "str") {
|
|
541
|
-
entry = `get: function () { return this.ref.readString(${
|
|
794
|
+
entry = `get: function () { return this.ref.readString(${resolve8}, ${f.strLen}); }`;
|
|
542
795
|
} else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
|
|
543
|
-
entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${
|
|
796
|
+
entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve8}); return v === null ? null : v.toString(); }`;
|
|
544
797
|
} else if (VEC_INFO[f.accessorKind]) {
|
|
545
798
|
const vi = VEC_INFO[f.accessorKind];
|
|
546
|
-
entry = `get: function () { var a = this.ref.readFloats(${
|
|
799
|
+
entry = `get: function () { var a = this.ref.readFloats(${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
547
800
|
} else {
|
|
548
|
-
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${
|
|
801
|
+
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve8}); }`;
|
|
549
802
|
}
|
|
550
803
|
if (f.writable) {
|
|
551
804
|
const wm = WRITE[f.accessorKind];
|
|
552
|
-
entry += `, set: function (v) { var o = ${
|
|
805
|
+
entry += `, set: function (v) { var o = ${resolve8}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
|
|
553
806
|
}
|
|
554
807
|
out.push(` ${S(f.propName)}: { ${entry} },`);
|
|
555
808
|
}
|
|
@@ -575,8 +828,8 @@ var LIST_PATH = "games/cs2/codegen-classes.json";
|
|
|
575
828
|
var JS_OUT = "games/cs2/js/schema.generated.js";
|
|
576
829
|
var DTS_OUT = "packages/cs2/schema.generated.d.ts";
|
|
577
830
|
function runGenSchema(repoRoot, opts) {
|
|
578
|
-
const catalog = JSON.parse(readFileSync5(
|
|
579
|
-
const list = JSON.parse(readFileSync5(
|
|
831
|
+
const catalog = JSON.parse(readFileSync5(join7(repoRoot, CATALOG_PATH), "utf8"));
|
|
832
|
+
const list = JSON.parse(readFileSync5(join7(repoRoot, LIST_PATH), "utf8"));
|
|
580
833
|
const model = buildModel(catalog, list);
|
|
581
834
|
const dts = emitDts(model);
|
|
582
835
|
const js = emitJs(model);
|
|
@@ -585,7 +838,7 @@ function runGenSchema(repoRoot, opts) {
|
|
|
585
838
|
const drift = [];
|
|
586
839
|
const files = [[JS_OUT, js], [DTS_OUT, dts]];
|
|
587
840
|
for (const [rel, content] of files) {
|
|
588
|
-
const abs =
|
|
841
|
+
const abs = join7(repoRoot, rel);
|
|
589
842
|
if (opts.check) {
|
|
590
843
|
let cur = "";
|
|
591
844
|
try {
|
|
@@ -605,7 +858,7 @@ function runGenSchema(repoRoot, opts) {
|
|
|
605
858
|
|
|
606
859
|
// src/eventgen/gen.ts
|
|
607
860
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
608
|
-
import { join as
|
|
861
|
+
import { join as join8 } from "node:path";
|
|
609
862
|
|
|
610
863
|
// src/eventgen/model.ts
|
|
611
864
|
var TYPE_TO_GETTER = {
|
|
@@ -690,11 +943,11 @@ function emitEventDts(model) {
|
|
|
690
943
|
var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
|
|
691
944
|
var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
|
|
692
945
|
function runGenEvents(repoRoot, opts) {
|
|
693
|
-
const catalog = JSON.parse(readFileSync6(
|
|
946
|
+
const catalog = JSON.parse(readFileSync6(join8(repoRoot, CATALOG_PATH2), "utf8"));
|
|
694
947
|
const model = buildEventModel(catalog);
|
|
695
948
|
const dts = emitEventDts(model);
|
|
696
949
|
const drift = [];
|
|
697
|
-
const abs =
|
|
950
|
+
const abs = join8(repoRoot, DTS_OUT2);
|
|
698
951
|
if (opts.check) {
|
|
699
952
|
let cur = "";
|
|
700
953
|
try {
|
|
@@ -710,7 +963,7 @@ function runGenEvents(repoRoot, opts) {
|
|
|
710
963
|
|
|
711
964
|
// src/navgen/gen.ts
|
|
712
965
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
713
|
-
import { join as
|
|
966
|
+
import { join as join9 } from "node:path";
|
|
714
967
|
|
|
715
968
|
// src/navgen/model.ts
|
|
716
969
|
var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -785,22 +1038,22 @@ function emitNavJs(model) {
|
|
|
785
1038
|
out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
|
|
786
1039
|
out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
|
|
787
1040
|
for (const f of w.fields) {
|
|
788
|
-
const
|
|
1041
|
+
const resolve8 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
|
|
789
1042
|
let getter;
|
|
790
1043
|
const kind = f.accessorKind;
|
|
791
1044
|
if (kind === "u64") {
|
|
792
|
-
getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${
|
|
1045
|
+
getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
|
|
793
1046
|
} else if (kind === "i64") {
|
|
794
|
-
getter = `get: function () { var v = this.root.readInt64Via(this.path, ${
|
|
1047
|
+
getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
|
|
795
1048
|
} else if (VEC_INFO[kind]) {
|
|
796
1049
|
const vi = VEC_INFO[kind];
|
|
797
|
-
getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${
|
|
1050
|
+
getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
798
1051
|
} else if (kind === "handle") {
|
|
799
|
-
getter = `get: function () { return this.root.readHandleVia(this.path, ${
|
|
1052
|
+
getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve8}); }`;
|
|
800
1053
|
} else {
|
|
801
1054
|
const method = READ_VIA[kind];
|
|
802
1055
|
if (!method) continue;
|
|
803
|
-
getter = `get: function () { return this.root.${method}(this.path, ${
|
|
1056
|
+
getter = `get: function () { return this.root.${method}(this.path, ${resolve8}); }`;
|
|
804
1057
|
}
|
|
805
1058
|
out.push(` ${S2(f.propName)}: { ${getter} },`);
|
|
806
1059
|
}
|
|
@@ -872,8 +1125,8 @@ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
|
|
|
872
1125
|
var JS_OUT2 = "games/cs2/js/nav.generated.js";
|
|
873
1126
|
var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
|
|
874
1127
|
function runGenNav(repoRoot, opts) {
|
|
875
|
-
const config = JSON.parse(readFileSync7(
|
|
876
|
-
const catalog = JSON.parse(readFileSync7(
|
|
1128
|
+
const config = JSON.parse(readFileSync7(join9(repoRoot, TARGETS_PATH), "utf8"));
|
|
1129
|
+
const catalog = JSON.parse(readFileSync7(join9(repoRoot, CATALOG_PATH3), "utf8"));
|
|
877
1130
|
const model = buildNavModel(config, catalog);
|
|
878
1131
|
const js = emitNavJs(model);
|
|
879
1132
|
const dts = emitNavDts(model);
|
|
@@ -881,7 +1134,7 @@ function runGenNav(repoRoot, opts) {
|
|
|
881
1134
|
const drift = [];
|
|
882
1135
|
const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
|
|
883
1136
|
for (const [rel, content] of files) {
|
|
884
|
-
const abs =
|
|
1137
|
+
const abs = join9(repoRoot, rel);
|
|
885
1138
|
if (opts.check) {
|
|
886
1139
|
let cur = "";
|
|
887
1140
|
try {
|
|
@@ -900,11 +1153,12 @@ function runGenNav(repoRoot, opts) {
|
|
|
900
1153
|
import {
|
|
901
1154
|
mkdirSync as mkdirSync2,
|
|
902
1155
|
writeFileSync as writeFileSync5,
|
|
903
|
-
existsSync as
|
|
1156
|
+
existsSync as existsSync6,
|
|
904
1157
|
readdirSync as readdirSync2,
|
|
905
|
-
readFileSync as readFileSync8
|
|
1158
|
+
readFileSync as readFileSync8,
|
|
1159
|
+
symlinkSync
|
|
906
1160
|
} from "node:fs";
|
|
907
|
-
import { dirname as dirname2, join as
|
|
1161
|
+
import { dirname as dirname2, join as join10, resolve as resolve6, basename } from "node:path";
|
|
908
1162
|
import { createInterface } from "node:readline/promises";
|
|
909
1163
|
import { stdin as input, stdout as output } from "node:process";
|
|
910
1164
|
import { spawnSync } from "node:child_process";
|
|
@@ -912,13 +1166,13 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
912
1166
|
function readCliVersion() {
|
|
913
1167
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
914
1168
|
const candidates = [
|
|
915
|
-
|
|
1169
|
+
join10(here, "..", "package.json"),
|
|
916
1170
|
// dist/cli.js → packages/sdk/package.json
|
|
917
|
-
|
|
1171
|
+
join10(here, "..", "..", "package.json")
|
|
918
1172
|
// src/create → packages/sdk/package.json
|
|
919
1173
|
];
|
|
920
1174
|
for (const p of candidates) {
|
|
921
|
-
if (!
|
|
1175
|
+
if (!existsSync6(p)) continue;
|
|
922
1176
|
try {
|
|
923
1177
|
const pkg = JSON.parse(readFileSync8(p, "utf8"));
|
|
924
1178
|
if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
|
|
@@ -930,18 +1184,18 @@ function readCliVersion() {
|
|
|
930
1184
|
function findLocalPackagesDir() {
|
|
931
1185
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
932
1186
|
const candidates = [
|
|
933
|
-
|
|
1187
|
+
join10(here, "..", ".."),
|
|
934
1188
|
// dist/cli.js → packages
|
|
935
|
-
|
|
1189
|
+
join10(here, "..", "..", "..")
|
|
936
1190
|
// src/create → packages
|
|
937
1191
|
];
|
|
938
1192
|
for (const c of candidates) {
|
|
939
|
-
if (isPackagesDir(c)) return
|
|
1193
|
+
if (isPackagesDir(c)) return resolve6(c);
|
|
940
1194
|
}
|
|
941
1195
|
return void 0;
|
|
942
1196
|
}
|
|
943
1197
|
function defaultNameFromPath(dir) {
|
|
944
|
-
const base = basename(
|
|
1198
|
+
const base = basename(resolve6(dir));
|
|
945
1199
|
const slug = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "plugin";
|
|
946
1200
|
return `@plugin/${slug}`;
|
|
947
1201
|
}
|
|
@@ -978,9 +1232,9 @@ function assertInstall(v) {
|
|
|
978
1232
|
}
|
|
979
1233
|
function createPackageNames(game) {
|
|
980
1234
|
if (game === "cs2") {
|
|
981
|
-
return ["sdk", "cs2"];
|
|
1235
|
+
return ["sdk", "cs2", "eslint-plugin"];
|
|
982
1236
|
}
|
|
983
|
-
return ["sdk"];
|
|
1237
|
+
return ["sdk", "eslint-plugin"];
|
|
984
1238
|
}
|
|
985
1239
|
function resolvePublishedVersion(pkg) {
|
|
986
1240
|
const r = spawnSync("npm", ["view", pkg, "version"], { encoding: "utf8", timeout: 5e3 });
|
|
@@ -990,68 +1244,62 @@ function versionSpecFrom(status, stdout) {
|
|
|
990
1244
|
const v = (stdout ?? "").trim();
|
|
991
1245
|
return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
|
|
992
1246
|
}
|
|
993
|
-
function registryDevDeps(game, sdkVersion,
|
|
1247
|
+
function registryDevDeps(game, sdkVersion, resolve8 = resolvePublishedVersion) {
|
|
994
1248
|
const deps = {};
|
|
995
1249
|
for (const n of createPackageNames(game)) {
|
|
996
|
-
deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` :
|
|
1250
|
+
deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve8(`@s2script/${n}`);
|
|
997
1251
|
}
|
|
998
1252
|
return deps;
|
|
999
1253
|
}
|
|
1000
1254
|
function fileDevDeps(packagesDir, game) {
|
|
1001
1255
|
const deps = {};
|
|
1002
1256
|
for (const n of createPackageNames(game)) {
|
|
1003
|
-
const abs =
|
|
1004
|
-
if (!
|
|
1257
|
+
const abs = join10(packagesDir, n);
|
|
1258
|
+
if (!existsSync6(join10(abs, "package.json"))) return void 0;
|
|
1005
1259
|
deps[`@s2script/${n}`] = `file:${abs}`;
|
|
1006
1260
|
}
|
|
1007
1261
|
return deps;
|
|
1008
1262
|
}
|
|
1009
1263
|
function pluginSource(game) {
|
|
1010
1264
|
if (game === "cs2") {
|
|
1011
|
-
return `import {
|
|
1265
|
+
return `import { plugin } from "@s2script/sdk/plugin";
|
|
1012
1266
|
import { Chat } from "@s2script/sdk/chat";
|
|
1013
1267
|
|
|
1014
|
-
export
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (
|
|
1018
|
-
Chat.toSlot(
|
|
1268
|
+
export default plugin((ctx) => {
|
|
1269
|
+
ctx.commands.register("hello", (cmd) => {
|
|
1270
|
+
cmd.reply("hello from s2script");
|
|
1271
|
+
if (cmd.callerSlot >= 0) {
|
|
1272
|
+
Chat.toSlot(cmd.callerSlot, "hello from s2script");
|
|
1019
1273
|
}
|
|
1020
1274
|
});
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
export function onUnload(): void {}
|
|
1275
|
+
});
|
|
1024
1276
|
`;
|
|
1025
1277
|
}
|
|
1026
|
-
return `import {
|
|
1278
|
+
return `import { plugin } from "@s2script/sdk/plugin";
|
|
1027
1279
|
import { delay } from "@s2script/sdk/timers";
|
|
1028
1280
|
|
|
1029
|
-
export
|
|
1281
|
+
export default plugin((ctx) => {
|
|
1030
1282
|
let n = 0;
|
|
1031
|
-
|
|
1283
|
+
ctx.server.onGameFrame(() => {
|
|
1032
1284
|
n += 1;
|
|
1033
1285
|
});
|
|
1034
|
-
void delay(1000).then(() =>
|
|
1035
|
-
|
|
1036
|
-
|
|
1286
|
+
void delay(1000).then(() => console.log("s2script plugin alive; frames so far:", n));
|
|
1287
|
+
});
|
|
1288
|
+
`;
|
|
1037
1289
|
}
|
|
1290
|
+
var ESLINT_RANGE = "^10.7.0";
|
|
1291
|
+
function eslintConfig() {
|
|
1292
|
+
return `import s2script from "@s2script/eslint-plugin";
|
|
1038
1293
|
|
|
1039
|
-
|
|
1294
|
+
// The SAME pinned rules \`s2s build\` enforces \u2014 the editor's ESLint extension picks this up,
|
|
1295
|
+
// so a violation is a red squiggle before you ever build (green editor => green build).
|
|
1296
|
+
export default s2script.configs.recommended({ tsconfigRootDir: import.meta.dirname });
|
|
1040
1297
|
`;
|
|
1041
1298
|
}
|
|
1042
1299
|
function tsconfigJson() {
|
|
1043
1300
|
return JSON.stringify(
|
|
1044
1301
|
{
|
|
1045
|
-
compilerOptions:
|
|
1046
|
-
strict: true,
|
|
1047
|
-
noEmit: true,
|
|
1048
|
-
moduleResolution: "bundler",
|
|
1049
|
-
module: "ESNext",
|
|
1050
|
-
target: "ES2020",
|
|
1051
|
-
lib: ["ES2020"],
|
|
1052
|
-
types: [],
|
|
1053
|
-
skipLibCheck: true
|
|
1054
|
-
},
|
|
1302
|
+
compilerOptions: sharedCompilerOptionsJson,
|
|
1055
1303
|
include: ["src", "node_modules/@s2script/sdk/globals.d.ts"]
|
|
1056
1304
|
},
|
|
1057
1305
|
null,
|
|
@@ -1060,7 +1308,10 @@ function tsconfigJson() {
|
|
|
1060
1308
|
}
|
|
1061
1309
|
function packageJsonContent(name, game, version, localPackagesDir) {
|
|
1062
1310
|
const fileDeps = localPackagesDir ? fileDevDeps(localPackagesDir, game) : void 0;
|
|
1063
|
-
const devDependencies =
|
|
1311
|
+
const devDependencies = {
|
|
1312
|
+
...fileDeps ?? registryDevDeps(game, version),
|
|
1313
|
+
eslint: ESLINT_RANGE
|
|
1314
|
+
};
|
|
1064
1315
|
return JSON.stringify(
|
|
1065
1316
|
{
|
|
1066
1317
|
name,
|
|
@@ -1070,10 +1321,7 @@ function packageJsonContent(name, game, version, localPackagesDir) {
|
|
|
1070
1321
|
scripts: {
|
|
1071
1322
|
build: "s2s build ."
|
|
1072
1323
|
},
|
|
1073
|
-
devDependencies
|
|
1074
|
-
s2script: {
|
|
1075
|
-
apiVersion: "1.x"
|
|
1076
|
-
}
|
|
1324
|
+
devDependencies
|
|
1077
1325
|
},
|
|
1078
1326
|
null,
|
|
1079
1327
|
2
|
|
@@ -1086,6 +1334,12 @@ dist/
|
|
|
1086
1334
|
.DS_Store
|
|
1087
1335
|
`;
|
|
1088
1336
|
}
|
|
1337
|
+
function linkLocalPackagesForNoInstall(targetPath, localPackagesDir) {
|
|
1338
|
+
const nm = join10(targetPath, "node_modules");
|
|
1339
|
+
mkdirSync2(nm, { recursive: true });
|
|
1340
|
+
const dest = join10(nm, "@s2script");
|
|
1341
|
+
if (!existsSync6(dest)) symlinkSync(localPackagesDir, dest);
|
|
1342
|
+
}
|
|
1089
1343
|
function runInstall(dir, pm) {
|
|
1090
1344
|
if (pm === "none") return;
|
|
1091
1345
|
const r = spawnSync(pm, ["install"], {
|
|
@@ -1097,7 +1351,7 @@ function runInstall(dir, pm) {
|
|
|
1097
1351
|
if (r.status !== 0) throw new Error(`${pm} install failed (exit ${r.status})`);
|
|
1098
1352
|
}
|
|
1099
1353
|
async function createPlugin(opts = {}) {
|
|
1100
|
-
const targetPath =
|
|
1354
|
+
const targetPath = resolve6(opts.path ?? ".");
|
|
1101
1355
|
const interactive = Boolean(input.isTTY && output.isTTY && !opts.yes);
|
|
1102
1356
|
let game = assertGame(opts.game);
|
|
1103
1357
|
let name = opts.name;
|
|
@@ -1106,7 +1360,7 @@ async function createPlugin(opts = {}) {
|
|
|
1106
1360
|
if (template !== "minimal") {
|
|
1107
1361
|
throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
|
|
1108
1362
|
}
|
|
1109
|
-
if (
|
|
1363
|
+
if (existsSync6(targetPath)) {
|
|
1110
1364
|
const kids = readdirSync2(targetPath);
|
|
1111
1365
|
const meaningful = kids.filter((k) => k !== ".git");
|
|
1112
1366
|
if (meaningful.length) {
|
|
@@ -1153,18 +1407,21 @@ async function createPlugin(opts = {}) {
|
|
|
1153
1407
|
install = install ?? (opts.noInstall ? "none" : "npm");
|
|
1154
1408
|
const version = readCliVersion();
|
|
1155
1409
|
const localPackagesDir = findLocalPackagesDir();
|
|
1156
|
-
mkdirSync2(
|
|
1410
|
+
mkdirSync2(join10(targetPath, "src"), { recursive: true });
|
|
1157
1411
|
writeFileSync5(
|
|
1158
|
-
|
|
1412
|
+
join10(targetPath, "package.json"),
|
|
1159
1413
|
packageJsonContent(name, game, version, localPackagesDir)
|
|
1160
1414
|
);
|
|
1161
|
-
writeFileSync5(
|
|
1162
|
-
writeFileSync5(
|
|
1163
|
-
writeFileSync5(
|
|
1415
|
+
writeFileSync5(join10(targetPath, "tsconfig.json"), tsconfigJson());
|
|
1416
|
+
writeFileSync5(join10(targetPath, "eslint.config.mjs"), eslintConfig());
|
|
1417
|
+
writeFileSync5(join10(targetPath, "src", "plugin.ts"), pluginSource(game));
|
|
1418
|
+
writeFileSync5(join10(targetPath, ".gitignore"), gitignore());
|
|
1164
1419
|
let installed = false;
|
|
1165
1420
|
if (install !== "none") {
|
|
1166
1421
|
runInstall(targetPath, install);
|
|
1167
1422
|
installed = true;
|
|
1423
|
+
} else if (localPackagesDir) {
|
|
1424
|
+
linkLocalPackagesForNoInstall(targetPath, localPackagesDir);
|
|
1168
1425
|
}
|
|
1169
1426
|
return {
|
|
1170
1427
|
dir: targetPath,
|
|
@@ -1176,13 +1433,85 @@ async function createPlugin(opts = {}) {
|
|
|
1176
1433
|
};
|
|
1177
1434
|
}
|
|
1178
1435
|
|
|
1436
|
+
// src/config/gen.ts
|
|
1437
|
+
import AdmZip2 from "adm-zip";
|
|
1438
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1439
|
+
import { join as join11 } from "node:path";
|
|
1440
|
+
|
|
1441
|
+
// src/config/config-path.ts
|
|
1442
|
+
function configFileName(id) {
|
|
1443
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_") + ".json";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/config/gen.ts
|
|
1447
|
+
function isDecl(v) {
|
|
1448
|
+
return typeof v.type === "string";
|
|
1449
|
+
}
|
|
1450
|
+
function generateDefaultJsonc(config) {
|
|
1451
|
+
return "{\n" + genEntries(config, 1) + "}\n";
|
|
1452
|
+
}
|
|
1453
|
+
function genEntries(entries, indent) {
|
|
1454
|
+
const pad = " ".repeat(indent);
|
|
1455
|
+
const keys = Object.keys(entries).filter((k) => !k.includes(".")).sort();
|
|
1456
|
+
let out = "";
|
|
1457
|
+
keys.forEach((key, i) => {
|
|
1458
|
+
const comma = i + 1 < keys.length ? "," : "";
|
|
1459
|
+
const raw = entries[key];
|
|
1460
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
1461
|
+
const obj = raw;
|
|
1462
|
+
if (isDecl(obj)) {
|
|
1463
|
+
const desc = typeof obj.description === "string" ? obj.description : "";
|
|
1464
|
+
out += `${pad}// ${String(obj.type)}${desc ? ` \u2014 ${desc}` : ""}
|
|
1465
|
+
`;
|
|
1466
|
+
out += `${pad}${JSON.stringify(key)}: ${JSON.stringify(obj.default)}${comma}
|
|
1467
|
+
`;
|
|
1468
|
+
} else {
|
|
1469
|
+
out += `${pad}${JSON.stringify(key)}: {
|
|
1470
|
+
`;
|
|
1471
|
+
out += genEntries(obj, indent + 1);
|
|
1472
|
+
out += `${pad}}${comma}
|
|
1473
|
+
`;
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
return out;
|
|
1477
|
+
}
|
|
1478
|
+
function genConfigFromManifest(manifest, outDir) {
|
|
1479
|
+
const config = manifest.config;
|
|
1480
|
+
if (config == null || typeof config !== "object" || Array.isArray(config) || Object.keys(config).length === 0) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
const id = typeof manifest.id === "string" ? manifest.id : "";
|
|
1484
|
+
if (!id) throw new Error("config gen: manifest declares config but has no id");
|
|
1485
|
+
mkdirSync3(outDir, { recursive: true });
|
|
1486
|
+
const path = join11(outDir, configFileName(id));
|
|
1487
|
+
writeFileSync6(path, generateDefaultJsonc(config));
|
|
1488
|
+
return path;
|
|
1489
|
+
}
|
|
1490
|
+
function genConfigForS2sp(s2spPath, outDir) {
|
|
1491
|
+
const zip = new AdmZip2(s2spPath);
|
|
1492
|
+
const entry = zip.getEntry("manifest.json");
|
|
1493
|
+
if (!entry) throw new Error(`config gen: ${s2spPath} has no manifest.json`);
|
|
1494
|
+
const manifest = JSON.parse(entry.getData().toString("utf8"));
|
|
1495
|
+
return genConfigFromManifest(manifest, outDir);
|
|
1496
|
+
}
|
|
1497
|
+
function runConfigGen(s2sps, outDir) {
|
|
1498
|
+
const written = [];
|
|
1499
|
+
const skipped = [];
|
|
1500
|
+
for (const p of s2sps) {
|
|
1501
|
+
const r = genConfigForS2sp(p, outDir);
|
|
1502
|
+
if (r) written.push(r);
|
|
1503
|
+
else skipped.push(p);
|
|
1504
|
+
}
|
|
1505
|
+
return { written, skipped };
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1179
1508
|
// src/cli.ts
|
|
1180
1509
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1181
|
-
import { dirname as dirname3, join as
|
|
1510
|
+
import { dirname as dirname3, join as join12, resolve as resolve7 } from "node:path";
|
|
1182
1511
|
var argv = process.argv.slice(2);
|
|
1183
1512
|
var command = argv[0];
|
|
1184
1513
|
function repoRootFromCli() {
|
|
1185
|
-
return
|
|
1514
|
+
return join12(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
|
|
1186
1515
|
}
|
|
1187
1516
|
function parseFlag(args, name) {
|
|
1188
1517
|
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
@@ -1194,6 +1523,20 @@ function parseFlag(args, name) {
|
|
|
1194
1523
|
function hasFlag(args, name) {
|
|
1195
1524
|
return args.includes(name);
|
|
1196
1525
|
}
|
|
1526
|
+
function positionals(args, flagsWithValue) {
|
|
1527
|
+
const out = [];
|
|
1528
|
+
for (let i = 0; i < args.length; i++) {
|
|
1529
|
+
const a = args[i];
|
|
1530
|
+
if (a.startsWith("--")) {
|
|
1531
|
+
const name = a.split("=")[0];
|
|
1532
|
+
if (flagsWithValue.includes(name) && !a.includes("=")) i++;
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
if (a.startsWith("-")) continue;
|
|
1536
|
+
out.push(a);
|
|
1537
|
+
}
|
|
1538
|
+
return out;
|
|
1539
|
+
}
|
|
1197
1540
|
if (command === "gen-schema") {
|
|
1198
1541
|
const repoRoot = repoRootFromCli();
|
|
1199
1542
|
const check = argv[1] === "--check";
|
|
@@ -1257,6 +1600,22 @@ if (command === "gen-schema") {
|
|
|
1257
1600
|
console.error(String(e instanceof Error ? e.message : e));
|
|
1258
1601
|
process.exit(1);
|
|
1259
1602
|
}
|
|
1603
|
+
} else if (command === "config" && argv[1] === "gen") {
|
|
1604
|
+
const args = argv.slice(2);
|
|
1605
|
+
const outDir = parseFlag(args, "--out") ?? process.cwd();
|
|
1606
|
+
const s2sps = positionals(args, ["--out"]);
|
|
1607
|
+
if (s2sps.length === 0) {
|
|
1608
|
+
console.error("Usage: s2s config gen <plugin.s2sp...> --out <dir>");
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
try {
|
|
1612
|
+
const { written, skipped } = runConfigGen(s2sps, outDir);
|
|
1613
|
+
for (const w of written) console.log(`config gen: wrote ${w}`);
|
|
1614
|
+
for (const s of skipped) console.log(`config gen: ${s} declares no config \u2014 skipped`);
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
console.error(String(e instanceof Error ? e.message : e));
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1260
1619
|
} else if (command === "build" && argv[1]) {
|
|
1261
1620
|
const args = argv.slice(1);
|
|
1262
1621
|
const dir = args.find((a) => !a.startsWith("-"));
|
|
@@ -1264,7 +1623,7 @@ if (command === "gen-schema") {
|
|
|
1264
1623
|
try {
|
|
1265
1624
|
const packagesDir = resolvePackagesDir({
|
|
1266
1625
|
explicit: packagesDirFlag,
|
|
1267
|
-
pluginDir:
|
|
1626
|
+
pluginDir: resolve7(dir),
|
|
1268
1627
|
fromCliUrl: import.meta.url
|
|
1269
1628
|
});
|
|
1270
1629
|
console.log(await buildPlugin(dir, packagesDir));
|
|
@@ -1274,7 +1633,7 @@ if (command === "gen-schema") {
|
|
|
1274
1633
|
}
|
|
1275
1634
|
} else {
|
|
1276
1635
|
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]"
|
|
1636
|
+
"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
1637
|
);
|
|
1279
1638
|
process.exit(1);
|
|
1280
1639
|
}
|