@polytric/openws-sdkgen 0.0.2 → 0.0.3

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/dist/main.cjs ADDED
@@ -0,0 +1,609 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // ../../node_modules/.pnpm/tsup@8.5.1_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
27
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+
30
+ // src/build-ir.ts
31
+ var import_schema = __toESM(require("@pocketgems/schema"), 1);
32
+ var validateIr = import_schema.default.obj({
33
+ package: import_schema.default.obj({
34
+ project: import_schema.default.str,
35
+ service: import_schema.default.str,
36
+ description: import_schema.default.str.optional(),
37
+ version: import_schema.default.str.optional()
38
+ }),
39
+ networks: import_schema.default.arr(
40
+ import_schema.default.obj({
41
+ name: import_schema.default.str,
42
+ description: import_schema.default.str.optional(),
43
+ version: import_schema.default.str.optional(),
44
+ roles: import_schema.default.arr(
45
+ import_schema.default.obj({
46
+ name: import_schema.default.str,
47
+ description: import_schema.default.str.optional(),
48
+ isHost: import_schema.default.bool,
49
+ endpoints: import_schema.default.arr(
50
+ import_schema.default.obj({
51
+ scheme: import_schema.default.str.enum("ws", "wss"),
52
+ host: import_schema.default.str,
53
+ port: import_schema.default.int.min(0).max(65535),
54
+ path: import_schema.default.str
55
+ })
56
+ )
57
+ })
58
+ ),
59
+ handlers: import_schema.default.arr(
60
+ import_schema.default.obj({
61
+ roleName: import_schema.default.str,
62
+ handlerName: import_schema.default.str,
63
+ description: import_schema.default.str.optional()
64
+ })
65
+ ),
66
+ messages: import_schema.default.arr(
67
+ import_schema.default.obj({
68
+ roleName: import_schema.default.str,
69
+ handlerName: import_schema.default.str,
70
+ description: import_schema.default.str.optional()
71
+ })
72
+ ),
73
+ models: import_schema.default.arr(
74
+ import_schema.default.obj({
75
+ scopeName: import_schema.default.str,
76
+ modelName: import_schema.default.str,
77
+ type: import_schema.default.str,
78
+ description: import_schema.default.str.optional(),
79
+ properties: import_schema.default.arr(
80
+ import_schema.default.obj({
81
+ type: import_schema.default.str,
82
+ scopeName: import_schema.default.str,
83
+ modelName: import_schema.default.str,
84
+ description: import_schema.default.str.optional(),
85
+ required: import_schema.default.bool.optional(),
86
+ items: import_schema.default.obj({
87
+ type: import_schema.default.str,
88
+ scopeName: import_schema.default.str,
89
+ modelName: import_schema.default.str,
90
+ description: import_schema.default.str.optional()
91
+ }).optional()
92
+ })
93
+ ).optional()
94
+ })
95
+ )
96
+ })
97
+ ).desc("An array of network definitions")
98
+ }).compile("IrValidator");
99
+ function buildIrModels(scopeName, modelName, schema) {
100
+ const type = schema.type;
101
+ const model = {
102
+ type,
103
+ scopeName,
104
+ modelName,
105
+ description: schema.description
106
+ };
107
+ switch (type) {
108
+ case "string":
109
+ case "number":
110
+ case "integer":
111
+ case "boolean":
112
+ case "null":
113
+ return [];
114
+ case "array": {
115
+ if (!schema.items) return [];
116
+ return buildIrModels(scopeName, modelName, schema.items);
117
+ }
118
+ case "object": {
119
+ const properties = [];
120
+ model.properties = properties;
121
+ const models = [];
122
+ if (!schema.properties) return [model];
123
+ for (const [subName, subSchema] of Object.entries(schema.properties)) {
124
+ const subModels = buildIrModels(scopeName, subName, subSchema);
125
+ const mainModel = subModels.find((m) => m.modelName === subName) ?? subSchema;
126
+ const property = {
127
+ type: mainModel.type,
128
+ description: mainModel.description,
129
+ scopeName: mainModel.scopeName ?? scopeName,
130
+ modelName: mainModel.modelName ?? subName,
131
+ required: schema.required?.includes(subName)
132
+ };
133
+ const itemsSource = mainModel.properties?.[0]?.items ?? mainModel.items;
134
+ if (itemsSource) {
135
+ property.items = {
136
+ type: itemsSource.type,
137
+ description: itemsSource.description,
138
+ scopeName: itemsSource?.scopeName ?? scopeName,
139
+ modelName: itemsSource?.modelName ?? subName
140
+ };
141
+ }
142
+ properties.push(property);
143
+ models.push(...subModels);
144
+ }
145
+ return [model, ...models];
146
+ }
147
+ default:
148
+ return [];
149
+ }
150
+ }
151
+ function buildIr(ctx) {
152
+ const { request, spec } = ctx;
153
+ if (!request) throw new Error("request is required");
154
+ if (!spec) throw new Error("spec is required");
155
+ const { hostRoles } = request;
156
+ const ir = {
157
+ package: {
158
+ project: request.project,
159
+ service: spec.name,
160
+ description: spec.description,
161
+ version: spec.version
162
+ },
163
+ networks: []
164
+ };
165
+ for (const [networkName, networkSpec] of Object.entries(spec.networks)) {
166
+ const hostRoleSpecs = hostRoles.map((hostRole) => networkSpec.roles[hostRole]);
167
+ const otherRoleSpecs = {};
168
+ for (const [roleName, roleSpec] of Object.entries(networkSpec.roles)) {
169
+ if (!hostRoles.includes(roleName)) {
170
+ otherRoleSpecs[roleName] = roleSpec;
171
+ }
172
+ }
173
+ const requiredRoles = /* @__PURE__ */ new Set();
174
+ for (const hostRoleSpec of hostRoleSpecs) {
175
+ for (const handlerSpec of Object.values(hostRoleSpec.messages)) {
176
+ if (handlerSpec.from) {
177
+ for (const fromRoleName of handlerSpec.from) {
178
+ requiredRoles.add(fromRoleName);
179
+ }
180
+ } else {
181
+ for (const key of Object.keys(otherRoleSpecs)) {
182
+ requiredRoles.add(key);
183
+ }
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ const irNetwork = {
189
+ name: networkName,
190
+ description: networkSpec.description,
191
+ version: networkSpec.version,
192
+ roles: [],
193
+ handlers: [],
194
+ messages: [],
195
+ models: []
196
+ };
197
+ for (const hostRoleSpec of hostRoleSpecs) {
198
+ irNetwork.roles.push({
199
+ name: hostRoleSpec.name,
200
+ description: hostRoleSpec.description,
201
+ isHost: true,
202
+ endpoints: hostRoleSpec.endpoints || []
203
+ });
204
+ for (const [handlerName, handlerSpec] of Object.entries(hostRoleSpec.messages)) {
205
+ irNetwork.handlers.push({
206
+ roleName: hostRoleSpec.name,
207
+ handlerName,
208
+ description: handlerSpec.description
209
+ });
210
+ irNetwork.models.push(
211
+ ...buildIrModels(hostRoleSpec.name, handlerName, handlerSpec.payload)
212
+ );
213
+ }
214
+ }
215
+ for (const [roleName, roleSpec] of Object.entries(otherRoleSpecs)) {
216
+ if (!requiredRoles.has(roleName)) continue;
217
+ irNetwork.roles.push({
218
+ name: roleName,
219
+ description: roleSpec.description,
220
+ isHost: false,
221
+ endpoints: roleSpec.endpoints || []
222
+ });
223
+ for (const [handlerName, handlerSpec] of Object.entries(roleSpec.messages)) {
224
+ irNetwork.messages.push({
225
+ roleName,
226
+ handlerName,
227
+ description: handlerSpec.description
228
+ });
229
+ irNetwork.models.push(...buildIrModels(roleName, handlerName, handlerSpec.payload));
230
+ }
231
+ }
232
+ ir.networks.push(irNetwork);
233
+ }
234
+ validateIr(ir);
235
+ return {
236
+ ...ctx,
237
+ ir
238
+ };
239
+ }
240
+
241
+ // src/dotnet/build-plan.ts
242
+ var import_node_path = __toESM(require("path"), 1);
243
+ var import_node_url = require("url");
244
+ var __dirname = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
245
+ function pascalCase(str) {
246
+ return str.charAt(0).toUpperCase() + str.slice(1);
247
+ }
248
+ function camelCase(str) {
249
+ return str.charAt(0).toLowerCase() + str.slice(1);
250
+ }
251
+ function createPlan(ctx) {
252
+ const { ir, request } = ctx;
253
+ if (!ir) throw new Error("ir is required");
254
+ if (!request) throw new Error("request is required");
255
+ const assemblyName = `${pascalCase(ir.package.project)}.${pascalCase(ir.package.service)}.Sdk`;
256
+ ir.assemblyName = assemblyName;
257
+ const plan = [
258
+ {
259
+ name: "assembly definition",
260
+ command: "render",
261
+ getData: () => ir,
262
+ template: import_node_path.default.join(__dirname, "template", "Service.asmdef.ejs"),
263
+ output: import_node_path.default.join(request.outputPath, assemblyName, `${assemblyName}.asmdef`)
264
+ },
265
+ {
266
+ name: "user assembly reference",
267
+ command: "render",
268
+ getData: () => ir,
269
+ template: import_node_path.default.join(__dirname, "template", "UserService.asmref.ejs"),
270
+ output: import_node_path.default.join(
271
+ request.outputPath,
272
+ `${assemblyName}.User`,
273
+ `${assemblyName}.User.asmref`
274
+ )
275
+ }
276
+ ];
277
+ for (const networkIr of ir.networks) {
278
+ const networkNamespace = `${pascalCase(ir.package.project)}.${pascalCase(ir.package.service)}.${pascalCase(networkIr.name)}`;
279
+ const networkClassName = `${pascalCase(networkIr.name)}Network`;
280
+ const networkOutputPath = import_node_path.default.join(
281
+ request.outputPath,
282
+ assemblyName,
283
+ pascalCase(networkIr.name)
284
+ );
285
+ const userNetworkOutputPath = import_node_path.default.join(
286
+ request.outputPath,
287
+ `${assemblyName}.User`,
288
+ pascalCase(networkIr.name)
289
+ );
290
+ const hostRoles = [];
291
+ const remoteRoles = [];
292
+ for (const role of networkIr.roles) {
293
+ const roleInfo = {
294
+ roleName: role.name,
295
+ className: pascalCase(role.name),
296
+ varName: camelCase(role.name),
297
+ description: role.description || "",
298
+ baseClassName: role.isHost ? "HostRole" : "RemoteRole",
299
+ endpoints: role.endpoints || []
300
+ };
301
+ if (role.isHost) {
302
+ hostRoles.push(roleInfo);
303
+ } else {
304
+ remoteRoles.push(roleInfo);
305
+ }
306
+ }
307
+ const allRoles = [...hostRoles, ...remoteRoles];
308
+ const allModelImports = allRoles.map((role) => `${networkNamespace}.Models.${role.className}`);
309
+ plan.push({
310
+ name: `network ${networkIr.name}`,
311
+ command: "render",
312
+ getData: () => ({
313
+ namespace: networkNamespace,
314
+ networkClassName,
315
+ networkName: networkIr.name,
316
+ description: networkIr.description,
317
+ version: networkIr.version,
318
+ allRoles
319
+ }),
320
+ template: import_node_path.default.join(__dirname, "template", "Network.cs.ejs"),
321
+ output: import_node_path.default.join(networkOutputPath, `${networkClassName}.cs`)
322
+ });
323
+ for (const modelIr of networkIr.models) {
324
+ modelIr.namespace = `${networkNamespace}.Models.${pascalCase(modelIr.scopeName)}`;
325
+ modelIr.className = pascalCase(modelIr.modelName);
326
+ if (modelIr.properties) {
327
+ for (const propertyIr of modelIr.properties) {
328
+ propertyIr.propertyName = pascalCase(propertyIr.modelName);
329
+ propertyIr.typeName = mapType(propertyIr);
330
+ }
331
+ }
332
+ }
333
+ for (const handlerIr of networkIr.handlers) {
334
+ handlerIr.modelClassName = pascalCase(handlerIr.handlerName);
335
+ handlerIr.messageName = handlerIr.handlerName;
336
+ handlerIr.methodName = pascalCase(handlerIr.handlerName);
337
+ }
338
+ for (const messageIr of networkIr.messages) {
339
+ messageIr.modelClassName = pascalCase(messageIr.handlerName);
340
+ messageIr.messageName = messageIr.handlerName;
341
+ messageIr.methodName = pascalCase(messageIr.handlerName);
342
+ }
343
+ for (const hostRole of hostRoles) {
344
+ const roleHandlers = networkIr.handlers.filter((h) => h.roleName === hostRole.roleName);
345
+ const modelImports = [`${networkNamespace}.Models.${hostRole.className}`];
346
+ plan.push({
347
+ name: `host role ${hostRole.className}`,
348
+ command: "render",
349
+ getData: () => ({
350
+ namespace: `${networkNamespace}.Roles`,
351
+ handlers: roleHandlers,
352
+ remoteRoles,
353
+ modelImports,
354
+ ...hostRole
355
+ }),
356
+ template: import_node_path.default.join(__dirname, "template", "HostRole.cs.ejs"),
357
+ output: import_node_path.default.join(networkOutputPath, "Roles", `${hostRole.className}.cs`)
358
+ });
359
+ plan.push({
360
+ name: `user host role ${hostRole.className}`,
361
+ command: "render",
362
+ getData: () => ({
363
+ namespace: `${networkNamespace}.Roles`,
364
+ handlers: roleHandlers,
365
+ remoteRoles,
366
+ modelImports: allModelImports,
367
+ ...hostRole
368
+ }),
369
+ template: import_node_path.default.join(__dirname, "template", "UserHostRole.cs.ejs"),
370
+ output: import_node_path.default.join(userNetworkOutputPath, "Roles", `${hostRole.className}.cs`)
371
+ });
372
+ }
373
+ for (const remoteRole of remoteRoles) {
374
+ const roleMessages = networkIr.messages.filter((m) => m.roleName === remoteRole.roleName);
375
+ const modelImports = [`${networkNamespace}.Models.${remoteRole.className}`];
376
+ plan.push({
377
+ name: `remote role ${remoteRole.className}`,
378
+ command: "render",
379
+ getData: () => ({
380
+ namespace: `${networkNamespace}.Roles`,
381
+ messages: roleMessages,
382
+ modelImports,
383
+ ...remoteRole
384
+ }),
385
+ template: import_node_path.default.join(__dirname, "template", "RemoteRole.cs.ejs"),
386
+ output: import_node_path.default.join(networkOutputPath, "Roles", `${remoteRole.className}.cs`)
387
+ });
388
+ }
389
+ for (const modelIr of networkIr.models) {
390
+ if (modelIr.type !== "object") continue;
391
+ plan.push({
392
+ name: `model ${modelIr.className}`,
393
+ command: "render",
394
+ getData: () => modelIr,
395
+ template: import_node_path.default.join(__dirname, "template", "Model.cs.ejs"),
396
+ output: import_node_path.default.join(
397
+ networkOutputPath,
398
+ "Models",
399
+ pascalCase(modelIr.scopeName),
400
+ `${modelIr.className}.cs`
401
+ )
402
+ });
403
+ }
404
+ }
405
+ return {
406
+ ...ctx,
407
+ plan
408
+ };
409
+ }
410
+ function mapType(property) {
411
+ switch (property.type) {
412
+ case "string":
413
+ return "string";
414
+ case "number":
415
+ return "double";
416
+ case "integer":
417
+ return "int";
418
+ case "boolean":
419
+ return "bool";
420
+ case "array":
421
+ if (!property.items) return "List<object>";
422
+ return `List<${mapType(property.items)}>`;
423
+ case "object":
424
+ return pascalCase(property.modelName);
425
+ default:
426
+ return "object";
427
+ }
428
+ }
429
+
430
+ // src/build-plan.ts
431
+ var planIndex = {
432
+ csharp: {
433
+ unity: createPlan
434
+ }
435
+ };
436
+ function dispatchBuildPlan(ctx) {
437
+ const { request } = ctx;
438
+ if (!request) throw new Error("request is required");
439
+ const language = Object.keys(request.target)[0];
440
+ const targetConfig = request.target[language];
441
+ if (!targetConfig) throw new Error(`No target config for language: ${language}`);
442
+ const environment = targetConfig.environment;
443
+ const plan = planIndex[language]?.[environment];
444
+ if (!plan) throw new Error(`No plan for ${language}/${environment}`);
445
+ return plan(ctx);
446
+ }
447
+
448
+ // src/build-request.ts
449
+ var import_node_path2 = __toESM(require("path"), 1);
450
+ var import_node_process = __toESM(require("process"), 1);
451
+ var import_schema2 = __toESM(require("@pocketgems/schema"), 1);
452
+ var validateBuildRequest = import_schema2.default.obj({
453
+ specPath: import_schema2.default.str,
454
+ outputPath: import_schema2.default.str,
455
+ project: import_schema2.default.str,
456
+ hostRoles: import_schema2.default.arr(import_schema2.default.str),
457
+ target: import_schema2.default.obj({
458
+ csharp: import_schema2.default.obj({
459
+ environment: import_schema2.default.str.enum("unity"),
460
+ frameworks: import_schema2.default.arr(import_schema2.default.str.enum("newtonsoft")).optional()
461
+ }).optional(),
462
+ javascript: import_schema2.default.obj({
463
+ environment: import_schema2.default.str.enum("node", "browser"),
464
+ frameworks: import_schema2.default.arr(import_schema2.default.str.enum("fastify")).optional()
465
+ }).optional()
466
+ }).min(1).max(1).desc("The target platform to generate code for")
467
+ }).compile("BuildRequestValidator");
468
+ function buildRequest(ctx) {
469
+ const { rawInput } = ctx;
470
+ if (!rawInput) throw new Error("rawInput is required");
471
+ console.log("Host roles:", rawInput.hostRole);
472
+ const request = {
473
+ specPath: import_node_path2.default.join(import_node_process.default.cwd(), rawInput.spec),
474
+ outputPath: import_node_path2.default.join(import_node_process.default.cwd(), rawInput.out),
475
+ project: rawInput.project,
476
+ hostRoles: rawInput.hostRole,
477
+ target: {
478
+ [rawInput.language]: {
479
+ environment: rawInput.environment,
480
+ frameworks: rawInput.frameworks
481
+ }
482
+ }
483
+ };
484
+ validateBuildRequest(request);
485
+ return {
486
+ ...ctx,
487
+ request
488
+ };
489
+ }
490
+
491
+ // src/execute-plan.ts
492
+ var import_node_fs = __toESM(require("fs"), 1);
493
+ var import_node_path3 = __toESM(require("path"), 1);
494
+ var import_ejs = __toESM(require("ejs"), 1);
495
+ var rendererCache = {};
496
+ function renderTemplate(templatePath, data) {
497
+ if (rendererCache[templatePath]) {
498
+ return rendererCache[templatePath](data);
499
+ }
500
+ const templateContent = import_node_fs.default.readFileSync(templatePath, "utf8");
501
+ const renderer = import_ejs.default.compile(templateContent);
502
+ rendererCache[templatePath] = renderer;
503
+ return renderer(data);
504
+ }
505
+ function executePlan(ctx) {
506
+ const { plan } = ctx;
507
+ if (!plan) throw new Error("plan is required");
508
+ for (const step of plan) {
509
+ switch (step.command) {
510
+ case "copy":
511
+ if (step.input) {
512
+ import_node_fs.default.cpSync(step.input, step.output, { recursive: true });
513
+ }
514
+ break;
515
+ case "render": {
516
+ const { getData, template, output } = step;
517
+ if (!getData || !template) continue;
518
+ const data = getData();
519
+ console.log(data);
520
+ import_node_fs.default.mkdirSync(import_node_path3.default.dirname(output), { recursive: true });
521
+ import_node_fs.default.writeFileSync(output, renderTemplate(template, { ctx: data }));
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ return ctx;
527
+ }
528
+
529
+ // src/load-spec.ts
530
+ var import_node_fs2 = __toESM(require("fs"), 1);
531
+ function loadSpec(ctx) {
532
+ const { request } = ctx;
533
+ if (!request) throw new Error("request is required");
534
+ const { specPath } = request;
535
+ const spec = JSON.parse(import_node_fs2.default.readFileSync(specPath, "utf8"));
536
+ return {
537
+ ...ctx,
538
+ spec
539
+ };
540
+ }
541
+
542
+ // src/parse-input.ts
543
+ var import_yargs = __toESM(require("yargs"), 1);
544
+ var import_helpers = require("yargs/helpers");
545
+ function parseInput(ctx) {
546
+ const args = (0, import_yargs.default)((0, import_helpers.hideBin)(ctx.argv)).scriptName("openws-sdkgen").version(false).option("spec", {
547
+ type: "string",
548
+ description: "The path to the OpenWS spec file",
549
+ demandOption: true
550
+ }).option("out", {
551
+ type: "string",
552
+ description: "The path to the output directory",
553
+ demandOption: true
554
+ }).option("project", {
555
+ type: "string",
556
+ description: "The project name",
557
+ demandOption: true
558
+ }).option("hostRole", {
559
+ type: "array",
560
+ string: true,
561
+ description: "The target participant roles that use the generated code",
562
+ demandOption: true
563
+ }).option("language", {
564
+ type: "string",
565
+ description: "The language to generate code for",
566
+ choices: ["csharp", "javascript"],
567
+ default: "csharp"
568
+ }).option("environment", {
569
+ type: "string",
570
+ description: "The environment to generate code for",
571
+ choices: ["unity", "node", "browser"],
572
+ default: "unity"
573
+ }).option("frameworks", {
574
+ type: "array",
575
+ string: true,
576
+ description: "The frameworks to generate code for",
577
+ choices: ["fastify", "newtonsoft"]
578
+ }).strict().help().parseSync();
579
+ return {
580
+ ...ctx,
581
+ rawInput: args
582
+ };
583
+ }
584
+
585
+ // src/prepare-output.ts
586
+ var import_node_fs3 = __toESM(require("fs"), 1);
587
+ function prepareOutput(ctx) {
588
+ const { request } = ctx;
589
+ if (!request) throw new Error("request is required");
590
+ const { outputPath } = request;
591
+ import_node_fs3.default.rmSync(outputPath, { recursive: true, force: true });
592
+ import_node_fs3.default.mkdirSync(outputPath, { recursive: true });
593
+ return ctx;
594
+ }
595
+
596
+ // src/main.ts
597
+ var Pipeline = [
598
+ parseInput,
599
+ buildRequest,
600
+ prepareOutput,
601
+ loadSpec,
602
+ buildIr,
603
+ dispatchBuildPlan,
604
+ executePlan
605
+ ];
606
+ function main() {
607
+ return Pipeline.reduce((acc, step) => step(acc), { argv: process.argv });
608
+ }
609
+ console.log(JSON.stringify(main(), null, 2));
package/package.json CHANGED
@@ -1,17 +1,14 @@
1
1
  {
2
2
  "name": "@polytric/openws-sdkgen",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "OpenWS SDK generator CLI",
5
- "license": "Apache-2.0",
6
- "author": "Polytric",
7
- "engines": {
8
- "node": ">=18"
9
- },
5
+ "type": "module",
10
6
  "bin": {
11
- "openws-sdkgen": "./src/main.js"
7
+ "openws-sdkgen": "./dist/main.cjs"
12
8
  },
13
9
  "files": [
14
- "src",
10
+ "dist",
11
+ "src/dotnet/template",
15
12
  "LICENSE",
16
13
  "README.md"
17
14
  ],
@@ -23,6 +20,11 @@
23
20
  "unity",
24
21
  "dotnet"
25
22
  ],
23
+ "author": "Polytric",
24
+ "license": "Apache-2.0",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
26
28
  "repository": {
27
29
  "type": "git",
28
30
  "url": "git+https://github.com/AgeOfLearning/openws.git",
@@ -36,5 +38,18 @@
36
38
  "ejs": "^3.1.10",
37
39
  "yargs": "^18.0.0"
38
40
  },
39
- "scripts": {}
41
+ "devDependencies": {
42
+ "@types/ejs": "^3.1.5",
43
+ "@types/node": "^22.15.29",
44
+ "@types/yargs": "^17.0.33",
45
+ "tsup": "^8.5.1",
46
+ "tsx": "^4.21.0",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "typecheck": "tsc --noEmit",
52
+ "generate": "tsx src/main.ts",
53
+ "test:csharp:unity": "tsx src/main.ts --spec ./test/spec.json --out ./generated/dotnet/unity --project Example --hostRole client --language csharp --environment unity"
54
+ }
40
55
  }
package/src/build-ir.js DELETED
@@ -1,220 +0,0 @@
1
- const S = require('@pocketgems/schema')
2
-
3
- const validateIr = S.obj({
4
- package: S.obj({
5
- project: S.str,
6
- service: S.str,
7
- description: S.str.optional(),
8
- version: S.str.optional(),
9
- }),
10
- networks: S.arr(
11
- S.obj({
12
- name: S.str,
13
- description: S.str.optional(),
14
- version: S.str.optional(),
15
- roles: S.arr(
16
- S.obj({
17
- name: S.str,
18
- description: S.str.optional(),
19
- isHost: S.bool,
20
- endpoints: S.arr(
21
- S.obj({
22
- scheme: S.str.enum('ws', 'wss'),
23
- host: S.str,
24
- port: S.int.min(0).max(65535),
25
- path: S.str,
26
- })
27
- ),
28
- })
29
- ),
30
- handlers: S.arr(
31
- S.obj({
32
- roleName: S.str,
33
- handlerName: S.str,
34
- description: S.str.optional(),
35
- })
36
- ),
37
- messages: S.arr(
38
- S.obj({
39
- roleName: S.str,
40
- handlerName: S.str,
41
- description: S.str.optional(),
42
- })
43
- ),
44
- models: S.arr(
45
- S.obj({
46
- scopeName: S.str,
47
- modelName: S.str,
48
- type: S.str,
49
- description: S.str.optional(),
50
- properties: S.arr(
51
- S.obj({
52
- type: S.str,
53
- scopeName: S.str,
54
- modelName: S.str,
55
- description: S.str.optional(),
56
- required: S.bool.optional(),
57
- items: S.obj({
58
- type: S.str,
59
- scopeName: S.str,
60
- modelName: S.str,
61
- description: S.str.optional(),
62
- }).optional(),
63
- })
64
- ).optional(),
65
- })
66
- ),
67
- })
68
- ).desc('An array of network definitions'),
69
- }).compile('IrValidator')
70
-
71
- function buildIrModels(scopeName, modelName, schema) {
72
- const type = schema.type
73
- const model = {
74
- type,
75
- scopeName,
76
- modelName,
77
- description: schema.description,
78
- }
79
- switch (type) {
80
- case 'string':
81
- case 'number':
82
- case 'integer':
83
- case 'boolean':
84
- case 'null':
85
- return []
86
- case 'array': {
87
- return buildIrModels(scopeName, modelName, schema.items)
88
- }
89
- case 'object': {
90
- const properties = []
91
- model.properties = properties
92
- const models = []
93
- for (const [subName, subSchema] of Object.entries(schema.properties)) {
94
- const subModels = buildIrModels(scopeName, subName, subSchema)
95
- const mainModel = subModels.find(m => m.modelName === subName) ?? subSchema
96
- const property = {
97
- type: mainModel.type,
98
- description: mainModel.description,
99
- scopeName: mainModel.scopeName ?? scopeName,
100
- modelName: mainModel.modelName ?? subName,
101
- required: schema.required?.includes(subName),
102
- }
103
-
104
- if (mainModel.items) {
105
- property.items = {
106
- type: mainModel.items.type,
107
- description: mainModel.items.description,
108
- scopeName: mainModel.items.scopeName ?? scopeName,
109
- modelName: mainModel.items.modelName ?? subName,
110
- }
111
- }
112
- properties.push(property)
113
- models.push(...subModels)
114
- }
115
- return [model, ...models]
116
- }
117
- }
118
- }
119
-
120
- module.exports = function buildIr(ctx) {
121
- const { request, spec } = ctx
122
- const { hostRoles } = request
123
-
124
- const irPackage = {
125
- project: request.project,
126
- service: spec.name,
127
- description: spec.description,
128
- version: spec.version,
129
- }
130
- const irNetworks = []
131
- const ir = {
132
- package: irPackage,
133
- networks: irNetworks,
134
- }
135
-
136
- for (const [networkName, networkSpec] of Object.entries(spec.networks)) {
137
- const hostRoleSpecs = hostRoles.map(hostRole => networkSpec.roles[hostRole])
138
- const otherRoleSpecs = {}
139
- for (const [roleName, roleSpec] of Object.entries(networkSpec.roles)) {
140
- if (!hostRoles.includes(roleName)) {
141
- otherRoleSpecs[roleName] = roleSpec
142
- }
143
- }
144
-
145
- const requiredRoles = new Set()
146
- for (const hostRoleSpec of hostRoleSpecs) {
147
- for (const handlerSpec of Object.values(hostRoleSpec.messages)) {
148
- if (handlerSpec.from) {
149
- for (const fromRoleName of handlerSpec.from) {
150
- requiredRoles.add(fromRoleName)
151
- }
152
- } else {
153
- requiredRoles.add(...Object.keys(otherRoleSpecs))
154
- break
155
- }
156
- }
157
- }
158
-
159
- const irRoles = []
160
- const irHandlers = []
161
- const irMessages = []
162
- const irModels = []
163
-
164
- // Add host roles
165
- for (const hostRoleSpec of hostRoleSpecs) {
166
- irRoles.push({
167
- name: hostRoleSpec.name,
168
- description: hostRoleSpec.description,
169
- isHost: true,
170
- endpoints: hostRoleSpec.endpoints || [],
171
- })
172
- for (const [handlerName, handlerSpec] of Object.entries(hostRoleSpec.messages)) {
173
- irHandlers.push({
174
- roleName: hostRoleSpec.name,
175
- handlerName,
176
- description: handlerSpec.description,
177
- })
178
- irModels.push(...buildIrModels(hostRoleSpec.name, handlerName, handlerSpec.payload))
179
- }
180
- }
181
-
182
- // Add remote roles
183
- for (const [roleName, roleSpec] of Object.entries(otherRoleSpecs)) {
184
- if (!requiredRoles.has(roleName)) {
185
- continue
186
- }
187
- irRoles.push({
188
- name: roleName,
189
- description: roleSpec.description,
190
- isHost: false,
191
- endpoints: roleSpec.endpoints || [],
192
- })
193
- for (const [handlerName, handlerSpec] of Object.entries(roleSpec.messages)) {
194
- irMessages.push({
195
- roleName,
196
- handlerName,
197
- description: handlerSpec.description,
198
- })
199
- irModels.push(...buildIrModels(roleName, handlerName, handlerSpec.payload))
200
- }
201
- }
202
-
203
- const irNetwork = {
204
- name: networkName,
205
- description: networkSpec.description,
206
- version: networkSpec.version,
207
- roles: irRoles,
208
- handlers: irHandlers,
209
- messages: irMessages,
210
- models: irModels,
211
- }
212
- irNetworks.push(irNetwork)
213
- }
214
-
215
- validateIr(ir)
216
- return {
217
- ...ctx,
218
- ir,
219
- }
220
- }
package/src/build-plan.js DELETED
@@ -1,16 +0,0 @@
1
- const path = require('node:path')
2
-
3
- const planIndex = {
4
- csharp: {
5
- unity: './dotnet/build-plan.js',
6
- },
7
- }
8
-
9
- module.exports = function dispatchBuildPlan(ctx) {
10
- const { request } = ctx
11
- const language = Object.keys(request.target)[0]
12
- const environment = request.target[language].environment
13
- const templatePath = planIndex[language][environment]
14
- const plan = require(path.join(__dirname, templatePath))
15
- return plan(ctx)
16
- }
@@ -1,46 +0,0 @@
1
- const path = require('node:path')
2
- const { cwd } = require('node:process')
3
-
4
- const S = require('@pocketgems/schema')
5
-
6
- const validateBuildRequest = S.obj({
7
- specPath: S.str,
8
- outputPath: S.str,
9
- project: S.str,
10
- hostRoles: S.arr(S.str),
11
- target: S.obj({
12
- csharp: S.obj({
13
- environment: S.str.enum('unity'), // aspnet
14
- frameworks: S.arr(S.str.enum('newtonsoft')).optional(),
15
- }).optional(),
16
- javascript: S.obj({
17
- environment: S.str.enum('node'), // browser
18
- frameworks: S.arr(S.str.enum('fastify')).optional(),
19
- }).optional(),
20
- })
21
- .min(1)
22
- .max(1)
23
- .desc('The target platform to generate code for'),
24
- }).compile('BuildRequestValidator')
25
-
26
- module.exports = function buildRequest(ctx) {
27
- const { rawInput } = ctx
28
- console.log(rawInput.hostRole)
29
- const request = {
30
- specPath: path.join(process.cwd(), rawInput.spec),
31
- outputPath: path.join(process.cwd(), rawInput.out),
32
- project: rawInput.project,
33
- hostRoles: rawInput.hostRole, // naming oddity from cli --hostRole serverA --hostRole serverB is readable as singular hostRole
34
- target: {
35
- [rawInput.language]: {
36
- environment: rawInput.environment,
37
- frameworks: rawInput.frameworks,
38
- },
39
- },
40
- }
41
- validateBuildRequest(request)
42
- return {
43
- ...ctx,
44
- request,
45
- }
46
- }
@@ -1,215 +0,0 @@
1
- const path = require('node:path')
2
-
3
- function pascalCase(str) {
4
- return str.charAt(0).toUpperCase() + str.slice(1)
5
- }
6
-
7
- function camelCase(str) {
8
- return str.charAt(0).toLowerCase() + str.slice(1)
9
- }
10
-
11
- function createPlan(ctx) {
12
- const { ir, request } = ctx
13
- const assemblyName = `${pascalCase(ir.package.project)}.${pascalCase(ir.package.service)}.Sdk`
14
- ir.assemblyName = assemblyName
15
-
16
- const plan = [
17
- {
18
- name: 'assembly definition',
19
- command: 'render',
20
- getData: () => ctx.ir,
21
- template: path.join(__dirname, 'template', 'Service.asmdef.ejs'),
22
- output: path.join(ctx.request.outputPath, assemblyName, `${assemblyName}.asmdef`),
23
- },
24
- {
25
- name: 'user assembly reference',
26
- command: 'render',
27
- getData: () => ctx.ir,
28
- template: path.join(__dirname, 'template', 'UserService.asmref.ejs'),
29
- output: path.join(
30
- ctx.request.outputPath,
31
- `${assemblyName}.User`,
32
- `${assemblyName}.User.asmref`
33
- ),
34
- },
35
- ]
36
-
37
- for (const networkIr of ir.networks) {
38
- const networkNamespace = `${pascalCase(ir.package.project)}.${pascalCase(ir.package.service)}.${pascalCase(networkIr.name)}`
39
- const networkClassName = `${pascalCase(networkIr.name)}Network`
40
- const networkOutputPath = path.join(
41
- ctx.request.outputPath,
42
- assemblyName,
43
- pascalCase(networkIr.name)
44
- )
45
- const userNetworkOutputPath = path.join(
46
- ctx.request.outputPath,
47
- `${assemblyName}.User`,
48
- pascalCase(networkIr.name)
49
- )
50
-
51
- // Build role info from IR
52
- const hostRoles = []
53
- const remoteRoles = []
54
-
55
- for (const role of networkIr.roles) {
56
- const roleInfo = {
57
- roleName: role.name,
58
- className: pascalCase(role.name),
59
- varName: camelCase(role.name),
60
- description: role.description || '',
61
- baseClassName: role.isHost ? 'HostRole' : 'RemoteRole',
62
- endpoints: role.endpoints || [],
63
- }
64
- if (role.isHost) {
65
- hostRoles.push(roleInfo)
66
- } else {
67
- remoteRoles.push(roleInfo)
68
- }
69
- }
70
-
71
- const allRoles = [...hostRoles, ...remoteRoles]
72
-
73
- // Build all model imports for user templates
74
- const allModelImports = allRoles.map(role => `${networkNamespace}.Models.${role.className}`)
75
-
76
- // Generate Network.cs
77
- plan.push({
78
- name: `network ${networkIr.name}`,
79
- command: 'render',
80
- getData: () => ({
81
- namespace: networkNamespace,
82
- networkClassName,
83
- networkName: networkIr.name,
84
- description: networkIr.description,
85
- version: networkIr.version,
86
- allRoles,
87
- }),
88
- template: path.join(__dirname, 'template', 'Network.cs.ejs'),
89
- output: path.join(networkOutputPath, `${networkClassName}.cs`),
90
- })
91
-
92
- // Process models
93
- for (const modelIr of networkIr.models) {
94
- modelIr.namespace = `${networkNamespace}.Models.${pascalCase(modelIr.scopeName)}`
95
- modelIr.className = pascalCase(modelIr.modelName)
96
- if (modelIr.properties) {
97
- for (const propertyIr of modelIr.properties) {
98
- propertyIr.propertyName = pascalCase(propertyIr.modelName)
99
- propertyIr.typeName = mapType(propertyIr)
100
- }
101
- }
102
- }
103
-
104
- // Process handlers (incoming messages for host roles)
105
- for (const handlerIr of networkIr.handlers) {
106
- handlerIr.modelClassName = pascalCase(handlerIr.handlerName)
107
- handlerIr.messageName = handlerIr.handlerName
108
- handlerIr.methodName = pascalCase(handlerIr.handlerName)
109
- }
110
-
111
- // Process messages (outgoing messages to remote roles)
112
- for (const messageIr of networkIr.messages) {
113
- messageIr.modelClassName = pascalCase(messageIr.handlerName)
114
- messageIr.messageName = messageIr.handlerName
115
- messageIr.methodName = pascalCase(messageIr.handlerName)
116
- }
117
-
118
- // Generate HostRole classes
119
- for (const hostRole of hostRoles) {
120
- const roleHandlers = networkIr.handlers.filter(h => h.roleName === hostRole.roleName)
121
- const modelImports = [`${networkNamespace}.Models.${hostRole.className}`]
122
-
123
- plan.push({
124
- name: `host role ${hostRole.className}`,
125
- command: 'render',
126
- getData: () => ({
127
- namespace: `${networkNamespace}.Roles`,
128
- handlers: roleHandlers,
129
- remoteRoles,
130
- modelImports,
131
- ...hostRole,
132
- }),
133
- template: path.join(__dirname, 'template', 'HostRole.cs.ejs'),
134
- output: path.join(networkOutputPath, 'Roles', `${hostRole.className}.cs`),
135
- })
136
-
137
- // Generate User stub for HostRole
138
- plan.push({
139
- name: `user host role ${hostRole.className}`,
140
- command: 'render',
141
- getData: () => ({
142
- namespace: `${networkNamespace}.Roles`,
143
- handlers: roleHandlers,
144
- remoteRoles,
145
- modelImports: allModelImports,
146
- ...hostRole,
147
- }),
148
- template: path.join(__dirname, 'template', 'UserHostRole.cs.ejs'),
149
- output: path.join(userNetworkOutputPath, 'Roles', `${hostRole.className}.cs`),
150
- })
151
- }
152
-
153
- // Generate RemoteRole classes
154
- for (const remoteRole of remoteRoles) {
155
- const roleMessages = networkIr.messages.filter(m => m.roleName === remoteRole.roleName)
156
- const modelImports = [`${networkNamespace}.Models.${remoteRole.className}`]
157
-
158
- plan.push({
159
- name: `remote role ${remoteRole.className}`,
160
- command: 'render',
161
- getData: () => ({
162
- namespace: `${networkNamespace}.Roles`,
163
- messages: roleMessages,
164
- modelImports,
165
- ...remoteRole,
166
- }),
167
- template: path.join(__dirname, 'template', 'RemoteRole.cs.ejs'),
168
- output: path.join(networkOutputPath, 'Roles', `${remoteRole.className}.cs`),
169
- })
170
- }
171
-
172
- // Generate Models
173
- for (const modelIr of networkIr.models) {
174
- if (modelIr.type !== 'object') continue
175
- plan.push({
176
- name: `model ${modelIr.className}`,
177
- command: 'render',
178
- getData: () => modelIr,
179
- template: path.join(__dirname, 'template', 'Model.cs.ejs'),
180
- output: path.join(
181
- networkOutputPath,
182
- 'Models',
183
- pascalCase(modelIr.scopeName),
184
- `${modelIr.className}.cs`
185
- ),
186
- })
187
- }
188
- }
189
-
190
- return {
191
- ...ctx,
192
- plan,
193
- }
194
- }
195
-
196
- function mapType(property) {
197
- switch (property.type) {
198
- case 'string':
199
- return 'string'
200
- case 'number':
201
- return 'double'
202
- case 'integer':
203
- return 'int'
204
- case 'boolean':
205
- return 'bool'
206
- case 'array':
207
- return `List<${mapType(property.items)}>`
208
- case 'object':
209
- return pascalCase(property.modelName)
210
- default:
211
- return 'object'
212
- }
213
- }
214
-
215
- module.exports = createPlan
@@ -1,38 +0,0 @@
1
- const fs = require('node:fs')
2
- const path = require('node:path')
3
-
4
- const ejs = require('ejs')
5
-
6
- const rendererCache = {}
7
-
8
- function renderTemplate(templatePath, data) {
9
- if (rendererCache[templatePath]) {
10
- return rendererCache[templatePath](data)
11
- }
12
- const templateContent = fs.readFileSync(templatePath, 'utf8')
13
- const renderer = ejs.compile(templateContent)
14
- rendererCache[templatePath] = renderer
15
- return renderer(data)
16
- }
17
-
18
- function executePlan(ctx) {
19
- const { plan } = ctx
20
- for (const step of plan) {
21
- switch (step.command) {
22
- case 'copy':
23
- fs.cpSync(step.input, step.output, { recursive: true })
24
- break
25
- case 'render': {
26
- const { getData, template, output } = step
27
- const data = getData()
28
- console.log(data)
29
- fs.mkdirSync(path.dirname(output), { recursive: true, force: true })
30
- fs.writeFileSync(output, renderTemplate(template, { ctx: data }))
31
- break
32
- }
33
- }
34
- }
35
- return ctx
36
- }
37
-
38
- module.exports = executePlan
package/src/load-spec.js DELETED
@@ -1,9 +0,0 @@
1
- module.exports = function loadSpec(ctx) {
2
- const { request } = ctx
3
- const { specPath } = request
4
- const spec = require(specPath)
5
- return {
6
- ...ctx,
7
- spec,
8
- }
9
- }
package/src/main.js DELETED
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const buildIr = require('./build-ir')
4
- const dispatchBuildPlan = require('./build-plan')
5
- const buildRequest = require('./build-request')
6
- const executePlan = require('./execute-plan')
7
- const loadSpec = require('./load-spec')
8
- const parseInput = require('./parse-input')
9
- const prepareOutput = require('./prepare-output')
10
-
11
- const Pipeline = [
12
- parseInput,
13
- buildRequest,
14
- prepareOutput,
15
- loadSpec,
16
- buildIr,
17
- dispatchBuildPlan,
18
- executePlan,
19
- ]
20
-
21
- function main() {
22
- return Pipeline.reduce(
23
- (acc, step) => {
24
- return step(acc)
25
- },
26
- { argv: process.argv }
27
- )
28
- }
29
-
30
- console.log(JSON.stringify(main(), null, 2))
@@ -1,49 +0,0 @@
1
- const yargs = require('yargs')
2
- const { hideBin } = require('yargs/helpers')
3
-
4
- module.exports = function parseInput(ctx) {
5
- const args = yargs(hideBin(ctx.argv))
6
- .scriptName('sdkgen')
7
- .version(false)
8
- .option('spec', {
9
- type: 'string',
10
- description: 'The path to the OpenWS spec file',
11
- })
12
- .option('out', {
13
- type: 'string',
14
- description: 'The path to the output directory',
15
- })
16
- .option('project', {
17
- type: 'string',
18
- description: 'The path to the project directory',
19
- })
20
- .option('hostRole', {
21
- type: 'array',
22
- description: 'The target participant roles that use the generated code',
23
- })
24
- .option('language', {
25
- type: 'string',
26
- description: 'The language to generate code for',
27
- choices: ['csharp', 'javascript'],
28
- default: 'csharp',
29
- })
30
- .option('environment', {
31
- type: 'string',
32
- description: 'The environments to generate code for',
33
- choices: ['unity', 'node', 'browser'],
34
- default: ['unity'],
35
- })
36
- .option('frameworks', {
37
- type: 'array',
38
- description: 'The frameworks to generate code for',
39
- choices: ['fastify', 'newtonsoft'],
40
- })
41
- .strict()
42
- .help()
43
- .parse()
44
-
45
- return {
46
- ...ctx,
47
- rawInput: args,
48
- }
49
- }
@@ -1,12 +0,0 @@
1
- const fs = require('node:fs')
2
-
3
- // 1. Create / clear output directory
4
- // 2. Load OpenWS spec
5
- // 3. Generate plan
6
- module.exports = function prepareOutput(ctx) {
7
- const { request } = ctx
8
- const { outputPath } = request
9
- fs.rmSync(outputPath, { recursive: true, force: true })
10
- fs.mkdirSync(outputPath, { recursive: true })
11
- return ctx
12
- }