@polytric/openws-sdkgen 0.0.5 → 0.0.6

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.
@@ -0,0 +1,532 @@
1
+ // src/plans/typescript.ts
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ var TEMPLATE_DIR = path.join(__dirname, "../templates/typescript");
6
+ var SRC_TEMPLATE_DIR = path.join(TEMPLATE_DIR, "src");
7
+ var CORE_TEMPLATE_DIR = path.join(SRC_TEMPLATE_DIR, "core");
8
+ var ROLES_TEMPLATE_DIR = path.join(CORE_TEMPLATE_DIR, "roles");
9
+ var MODELS_TEMPLATE_DIR = path.join(CORE_TEMPLATE_DIR, "models");
10
+ var SDK_TEMPLATE_DIR = path.join(SRC_TEMPLATE_DIR, "sdk");
11
+ function pascalCase(str) {
12
+ return str.charAt(0).toUpperCase() + str.slice(1);
13
+ }
14
+ function camelCase(str) {
15
+ return str.charAt(0).toLowerCase() + str.slice(1);
16
+ }
17
+ function kebabCase(str) {
18
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
19
+ }
20
+ function createPlan(ctx) {
21
+ const { ir, request, spec } = ctx;
22
+ if (!ir) throw new Error("ir is required");
23
+ if (!request) throw new Error("request is required");
24
+ if (!spec) throw new Error("spec is required");
25
+ const language = Object.keys(request.target)[0];
26
+ const isTypeScript = language === "typescript";
27
+ const extension = isTypeScript ? "ts" : "js";
28
+ const packageName = `@${kebabCase(ir.package.project)}/${kebabCase(ir.package.service)}-openws-sdk`;
29
+ const plan = [
30
+ {
31
+ name: `${language} package manifest`,
32
+ command: "render",
33
+ getData: () => ({
34
+ isTypeScript,
35
+ packageName,
36
+ description: ir.package.description,
37
+ version: ir.package.version ?? "0.0.0",
38
+ extension
39
+ }),
40
+ template: path.join(TEMPLATE_DIR, "package.json.ejs"),
41
+ output: path.join(request.outputPath, "package.json")
42
+ }
43
+ ];
44
+ if (isTypeScript) {
45
+ plan.push(
46
+ {
47
+ name: `${language} tsconfig`,
48
+ command: "render",
49
+ getData: () => ({}),
50
+ template: path.join(TEMPLATE_DIR, "tsconfig.json.ejs"),
51
+ output: path.join(request.outputPath, "tsconfig.json")
52
+ },
53
+ {
54
+ name: `${language} tsup config`,
55
+ command: "render",
56
+ getData: () => ({}),
57
+ template: path.join(TEMPLATE_DIR, "tsup.config.ts.ejs"),
58
+ output: path.join(request.outputPath, "tsup.config.ts")
59
+ }
60
+ );
61
+ }
62
+ const networkExports = [];
63
+ for (const [networkName, networkSpec] of Object.entries(spec.networks)) {
64
+ const networkFileName = kebabCase(networkName);
65
+ const networkOutputPath = path.join(request.outputPath, "src", networkFileName);
66
+ const sdkOutputPath = path.join(request.outputPath, "src", "sdk");
67
+ const allRoles = Object.values(networkSpec.roles).map(toRoleInfo);
68
+ const rolesByName = new Map(allRoles.map((role) => [role.roleName, role]));
69
+ const hostRoles = allRoles;
70
+ const modelScopes = buildModelScopes(buildSpecModels(networkSpec));
71
+ networkExports.push({
72
+ exportName: camelCase(networkName),
73
+ fileName: networkFileName
74
+ });
75
+ plan.push({
76
+ name: `${language} network ${networkName}`,
77
+ command: "render",
78
+ getData: () => ({
79
+ isTypeScript,
80
+ networkName,
81
+ networkClassName: `${pascalCase(networkName)}Network`,
82
+ description: networkSpec.description,
83
+ version: networkSpec.version,
84
+ hostRoles,
85
+ allRoles,
86
+ extension
87
+ }),
88
+ template: path.join(CORE_TEMPLATE_DIR, "network.ts.ejs"),
89
+ output: path.join(networkOutputPath, `network.${extension}`)
90
+ });
91
+ plan.push({
92
+ name: `${language} network exports ${networkName}`,
93
+ command: "render",
94
+ getData: () => ({
95
+ isTypeScript,
96
+ extension,
97
+ modelScopes
98
+ }),
99
+ template: path.join(CORE_TEMPLATE_DIR, "index.ts.ejs"),
100
+ output: path.join(networkOutputPath, `index.${extension}`)
101
+ });
102
+ const roleMessagesByName = /* @__PURE__ */ new Map();
103
+ for (const role of allRoles) {
104
+ const roleSpec = networkSpec.roles[role.roleName];
105
+ const messages = Object.entries(roleSpec.messages).map(
106
+ ([messageName, messageSpec]) => toMessageInfo(messageName, messageSpec, rolesByName)
107
+ );
108
+ roleMessagesByName.set(role.roleName, messages);
109
+ }
110
+ const rolesWithMessages = allRoles.map((role) => ({
111
+ ...role,
112
+ messages: roleMessagesByName.get(role.roleName) ?? []
113
+ }));
114
+ for (const role of rolesWithMessages) {
115
+ plan.push({
116
+ name: `${language} core role ${role.className}`,
117
+ command: "render",
118
+ getData: () => ({
119
+ isTypeScript,
120
+ extension,
121
+ peerRoles: allRoles.filter((peerRole) => peerRole.roleName !== role.roleName),
122
+ ...role
123
+ }),
124
+ template: path.join(ROLES_TEMPLATE_DIR, "role.ts.ejs"),
125
+ output: path.join(networkOutputPath, "roles", `${role.fileName}.${extension}`)
126
+ });
127
+ }
128
+ plan.push({
129
+ name: `${language} core role exports ${networkName}`,
130
+ command: "render",
131
+ getData: () => ({
132
+ isTypeScript,
133
+ extension,
134
+ roles: allRoles
135
+ }),
136
+ template: path.join(ROLES_TEMPLATE_DIR, "index.ts.ejs"),
137
+ output: path.join(networkOutputPath, "roles", `index.${extension}`)
138
+ });
139
+ for (const modelScope of modelScopes) {
140
+ plan.push({
141
+ name: `${language} model exports ${modelScope.scopeName}`,
142
+ command: "render",
143
+ getData: () => ({
144
+ isTypeScript,
145
+ extension,
146
+ ...modelScope
147
+ }),
148
+ template: path.join(MODELS_TEMPLATE_DIR, "index.ts.ejs"),
149
+ output: path.join(
150
+ networkOutputPath,
151
+ "models",
152
+ modelScope.fileName,
153
+ `index.${extension}`
154
+ )
155
+ });
156
+ for (const model of modelScope.models) {
157
+ plan.push({
158
+ name: `${language} model ${model.className}`,
159
+ command: "render",
160
+ getData: () => ({
161
+ isTypeScript,
162
+ ...model
163
+ }),
164
+ template: path.join(MODELS_TEMPLATE_DIR, "model.ts.ejs"),
165
+ output: path.join(
166
+ networkOutputPath,
167
+ "models",
168
+ modelScope.fileName,
169
+ `${model.fileName}.${extension}`
170
+ )
171
+ });
172
+ }
173
+ }
174
+ for (const hostRole of hostRoles) {
175
+ const remoteRoles = getPeerRoles(networkSpec, rolesByName, hostRole.roleName).map(
176
+ (remoteRole) => ({
177
+ ...remoteRole,
178
+ scopedApiName: `${hostRole.className}${remoteRole.className}Api`,
179
+ allowedMethodNames: getAllowedMessageMethodNames(
180
+ networkSpec,
181
+ remoteRole.roleName,
182
+ hostRole.roleName
183
+ )
184
+ })
185
+ );
186
+ const roleSpec = networkSpec.roles[hostRole.roleName];
187
+ const roleHandlers = Object.entries(roleSpec.messages).map(
188
+ ([messageName, messageSpec]) => toHandlerInfo(
189
+ messageName,
190
+ messageSpec,
191
+ rolesByName,
192
+ hostRole.roleName,
193
+ allRoles
194
+ )
195
+ );
196
+ plan.push({
197
+ name: `${language} sdk role ${hostRole.className}`,
198
+ command: "render",
199
+ getData: () => ({
200
+ isTypeScript,
201
+ extension,
202
+ handlers: roleHandlers,
203
+ networkName,
204
+ networkDescription: networkSpec.description,
205
+ networkVersion: networkSpec.version,
206
+ remoteRoles,
207
+ ...hostRole
208
+ }),
209
+ template: path.join(SDK_TEMPLATE_DIR, "role.ts.ejs"),
210
+ output: path.join(sdkOutputPath, `${hostRole.fileName}.${extension}`)
211
+ });
212
+ }
213
+ plan.push({
214
+ name: `${language} sdk exports ${networkName}`,
215
+ command: "render",
216
+ getData: () => ({
217
+ isTypeScript,
218
+ extension,
219
+ roles: hostRoles
220
+ }),
221
+ template: path.join(SDK_TEMPLATE_DIR, "index.ts.ejs"),
222
+ output: path.join(sdkOutputPath, `index.${extension}`)
223
+ });
224
+ }
225
+ plan.push({
226
+ name: `${language} package exports`,
227
+ command: "render",
228
+ getData: () => ({
229
+ isTypeScript,
230
+ extension,
231
+ networkExports
232
+ }),
233
+ template: path.join(SRC_TEMPLATE_DIR, "index.ts.ejs"),
234
+ output: path.join(request.outputPath, "src", `index.${extension}`)
235
+ });
236
+ return {
237
+ ...ctx,
238
+ plan
239
+ };
240
+ }
241
+ function buildModelScopes(models) {
242
+ const scopes = /* @__PURE__ */ new Map();
243
+ const objectModels = models.filter((model) => model.type === "object");
244
+ for (const model of objectModels) {
245
+ const scopeName = model.scopeName;
246
+ const scope = scopes.get(scopeName) ?? {
247
+ scopeName,
248
+ className: pascalCase(scopeName),
249
+ varName: camelCase(scopeName),
250
+ fileName: kebabCase(scopeName),
251
+ models: []
252
+ };
253
+ scope.models.push({
254
+ scopeName,
255
+ className: pascalCase(model.modelName),
256
+ fileName: kebabCase(model.modelName),
257
+ schema: buildModelSchema(model, objectModels),
258
+ properties: (model.properties ?? []).map((property) => ({
259
+ name: property.modelName,
260
+ optional: !property.required,
261
+ typeName: mapType(property)
262
+ })),
263
+ imports: buildModelImports(model)
264
+ });
265
+ scopes.set(scopeName, scope);
266
+ }
267
+ return [...scopes.values()];
268
+ }
269
+ function toRoleInfo(role) {
270
+ const className = pascalCase(role.name);
271
+ const fileName = kebabCase(role.name);
272
+ return {
273
+ roleName: role.name,
274
+ className,
275
+ roleClassName: className,
276
+ hostRoleClassName: `${className}Host`,
277
+ apiName: `${className}Api`,
278
+ varName: camelCase(role.name),
279
+ apiVarName: `${camelCase(role.name)}Api`,
280
+ fileName,
281
+ roleFileName: `${fileName}-role`,
282
+ description: role.description || "",
283
+ endpoints: role.endpoints || []
284
+ };
285
+ }
286
+ function getMessageFromRoles(message, rolesByName, currentRoleName, allRoles) {
287
+ const fromRoleNames = getMessageFromRoleNames(
288
+ message,
289
+ currentRoleName,
290
+ allRoles.map((role) => role.roleName)
291
+ );
292
+ return fromRoleNames.map((roleName) => rolesByName.get(roleName)).filter((role) => Boolean(role));
293
+ }
294
+ function getExplicitMessageFromRoles(message, rolesByName) {
295
+ if (!message.from) return void 0;
296
+ return message.from.map((roleName) => rolesByName.get(roleName)).filter((role) => Boolean(role));
297
+ }
298
+ function getPeerRoles(network, rolesByName, hostRoleName) {
299
+ const peers = /* @__PURE__ */ new Set();
300
+ const hostRole = network.roles[hostRoleName];
301
+ const allRoleNames = Object.keys(network.roles);
302
+ for (const message of Object.values(hostRole.messages)) {
303
+ for (const roleName of getMessageFromRoleNames(message, hostRoleName, allRoleNames)) {
304
+ peers.add(roleName);
305
+ }
306
+ }
307
+ for (const [roleName, role] of Object.entries(network.roles)) {
308
+ if (roleName === hostRoleName) continue;
309
+ for (const message of Object.values(role.messages)) {
310
+ if (getMessageFromRoleNames(message, roleName, allRoleNames).includes(hostRoleName)) {
311
+ peers.add(roleName);
312
+ }
313
+ }
314
+ }
315
+ return [...peers].map((roleName) => rolesByName.get(roleName)).filter((role) => Boolean(role));
316
+ }
317
+ function getMessageFromRoleNames(message, targetRoleName, allRoleNames) {
318
+ return message.from ?? allRoleNames.filter((roleName) => roleName !== targetRoleName);
319
+ }
320
+ function getAllowedMessageMethodNames(network, targetRoleName, fromRoleName) {
321
+ const targetRole = network.roles[targetRoleName];
322
+ const allRoleNames = Object.keys(network.roles);
323
+ return Object.entries(targetRole.messages).filter(
324
+ ([, message]) => getMessageFromRoleNames(message, targetRoleName, allRoleNames).includes(fromRoleName)
325
+ ).map(([messageName]) => camelCase(messageName));
326
+ }
327
+ function buildSpecModels(network) {
328
+ const models = [];
329
+ for (const role of Object.values(network.roles)) {
330
+ for (const [messageName, message] of Object.entries(role.messages)) {
331
+ models.push(...buildIrModels(role.name, `${messageName}Payload`, message.payload));
332
+ }
333
+ }
334
+ return models;
335
+ }
336
+ function buildIrModels(scopeName, modelName, schema) {
337
+ const type = schema.type;
338
+ const model = {
339
+ type,
340
+ scopeName,
341
+ modelName,
342
+ description: schema.description
343
+ };
344
+ switch (type) {
345
+ case "string":
346
+ case "number":
347
+ case "integer":
348
+ case "boolean":
349
+ case "null":
350
+ return [];
351
+ case "array": {
352
+ const items = schema.items;
353
+ if (!items) return [];
354
+ return buildIrModels(scopeName, modelName, items);
355
+ }
356
+ case "object": {
357
+ const properties = [];
358
+ model.properties = properties;
359
+ const models = [];
360
+ const schemaProperties = schema.properties;
361
+ if (!schemaProperties) return [model];
362
+ for (const [subName, subSchema] of Object.entries(schemaProperties)) {
363
+ const subModels = buildIrModels(scopeName, subName, subSchema);
364
+ const mainModel = subModels.find((m) => m.modelName === subName) ?? subSchema;
365
+ const property = {
366
+ type: mainModel.type,
367
+ description: mainModel.description,
368
+ scopeName: mainModel.scopeName ?? scopeName,
369
+ modelName: mainModel.modelName ?? subName,
370
+ required: schema.required?.includes(subName)
371
+ };
372
+ const itemsSource = mainModel.properties?.[0]?.items ?? mainModel.items;
373
+ if (itemsSource) {
374
+ property.items = {
375
+ type: itemsSource.type,
376
+ description: itemsSource.description,
377
+ scopeName: itemsSource?.scopeName ?? scopeName,
378
+ modelName: itemsSource?.modelName ?? subName
379
+ };
380
+ }
381
+ properties.push(property);
382
+ models.push(...subModels);
383
+ }
384
+ return [model, ...models];
385
+ }
386
+ default:
387
+ return [];
388
+ }
389
+ }
390
+ function buildModelImports(model) {
391
+ const imports = /* @__PURE__ */ new Map();
392
+ const addImport = (modelName) => {
393
+ if (!modelName || modelName === model.modelName) return;
394
+ const className = pascalCase(modelName);
395
+ imports.set(className, {
396
+ className,
397
+ fileName: kebabCase(modelName)
398
+ });
399
+ };
400
+ for (const property of model.properties ?? []) {
401
+ if (property.type === "object") {
402
+ addImport(property.modelName);
403
+ }
404
+ if (property.type === "array" && property.items?.type === "object") {
405
+ addImport(property.items.modelName);
406
+ }
407
+ }
408
+ return [...imports.values()];
409
+ }
410
+ function buildModelSchema(model, models, seen = /* @__PURE__ */ new Set()) {
411
+ const key = `${model.scopeName}:${model.modelName}`;
412
+ if (seen.has(key)) return { type: "object" };
413
+ const nextSeen = new Set(seen);
414
+ nextSeen.add(key);
415
+ const properties = {};
416
+ const required = [];
417
+ for (const property of model.properties ?? []) {
418
+ properties[property.modelName] = buildPropertySchema(property, models, nextSeen);
419
+ if (property.required) required.push(property.modelName);
420
+ }
421
+ const schema = {
422
+ $schema: "http://json-schema.org/draft-07/schema#",
423
+ type: "object",
424
+ properties,
425
+ additionalProperties: false
426
+ };
427
+ if (model.description) schema.description = model.description;
428
+ if (required.length > 0) schema.required = required;
429
+ return schema;
430
+ }
431
+ function buildNestedObjectSchema(model, models, seen) {
432
+ const { $schema: _schema, ...schema } = buildModelSchema(model, models, seen);
433
+ return schema;
434
+ }
435
+ function buildPropertySchema(property, models, seen) {
436
+ const schema = buildSchemaForType(
437
+ property.type,
438
+ property.scopeName,
439
+ property.modelName,
440
+ models,
441
+ seen
442
+ );
443
+ if (property.description) schema.description = property.description;
444
+ if (property.type === "array" && property.items) {
445
+ schema.items = buildSchemaForType(
446
+ property.items.type,
447
+ property.items.scopeName,
448
+ property.items.modelName,
449
+ models,
450
+ seen
451
+ );
452
+ if (property.items.description && typeof schema.items === "object" && schema.items !== null) {
453
+ const itemSchema = schema.items;
454
+ itemSchema.description = property.items.description;
455
+ }
456
+ }
457
+ return schema;
458
+ }
459
+ function buildSchemaForType(type, scopeName, modelName, models, seen) {
460
+ switch (type) {
461
+ case "string":
462
+ case "number":
463
+ case "integer":
464
+ case "boolean":
465
+ case "null":
466
+ return { type };
467
+ case "array":
468
+ return { type: "array" };
469
+ case "object": {
470
+ const model = findModel(models, scopeName, modelName);
471
+ if (!model) return { type: "object" };
472
+ return buildNestedObjectSchema(model, models, seen);
473
+ }
474
+ default:
475
+ return {};
476
+ }
477
+ }
478
+ function findModel(models, scopeName, modelName) {
479
+ return models.find((model) => model.scopeName === scopeName && model.modelName === modelName);
480
+ }
481
+ function toHandlerInfo(messageName, message, rolesByName, _currentRoleName, _allRoles) {
482
+ const payloadType = pascalCase(messageName) + "Payload";
483
+ const methodSuffix = pascalCase(messageName);
484
+ return {
485
+ dispatchMethodName: camelCase(messageName),
486
+ onMethodName: `on${methodSuffix}`,
487
+ listenerFieldName: `${camelCase(messageName)}Handlers`,
488
+ messageName,
489
+ payloadType,
490
+ payloadFileName: kebabCase(payloadType),
491
+ schema: toJsonValue(message.payload),
492
+ fromRoles: getExplicitMessageFromRoles(message, rolesByName),
493
+ bindFromRoles: getMessageFromRoles(message, rolesByName, _currentRoleName, _allRoles)
494
+ };
495
+ }
496
+ function toMessageInfo(messageName, message, rolesByName) {
497
+ const payloadType = pascalCase(messageName) + "Payload";
498
+ return {
499
+ methodName: camelCase(messageName),
500
+ messageName,
501
+ payloadType,
502
+ payloadFileName: kebabCase(payloadType),
503
+ schema: toJsonValue(message.payload),
504
+ fromRoles: getExplicitMessageFromRoles(message, rolesByName)
505
+ };
506
+ }
507
+ function toJsonValue(value) {
508
+ return JSON.parse(JSON.stringify(value));
509
+ }
510
+ function mapType(property) {
511
+ switch (property.type) {
512
+ case "string":
513
+ return "string";
514
+ case "number":
515
+ case "integer":
516
+ return "number";
517
+ case "boolean":
518
+ return "boolean";
519
+ case "null":
520
+ return "null";
521
+ case "array":
522
+ if (!property.items) return "unknown[]";
523
+ return `${mapType(property.items)}[]`;
524
+ case "object":
525
+ return pascalCase(property.modelName);
526
+ default:
527
+ return "unknown";
528
+ }
529
+ }
530
+ export {
531
+ createPlan as default
532
+ };
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": <%- JSON.stringify(ctx.packageName) %>,
3
+ "version": <%- JSON.stringify(ctx.version) %>,
4
+ "description": <%- JSON.stringify(ctx.description ?? '') %>,
5
+ "type": "module",
6
+ <% if (ctx.isTypeScript) { -%>
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@polytric/openws": "^0.0.4"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "devDependencies": {
26
+ "tsup": "^8.5.1",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ <% } else { -%>
30
+ "main": "./src/index.js",
31
+ "exports": {
32
+ ".": "./src/index.js"
33
+ },
34
+ "files": [
35
+ "src"
36
+ ],
37
+ "dependencies": {
38
+ "@polytric/openws": "^0.0.4"
39
+ }
40
+ <% } -%>
41
+ }
@@ -0,0 +1,6 @@
1
+ export * from './network<%= ctx.isTypeScript ? '' : '.js' %>'
2
+ export * from './roles<%= ctx.isTypeScript ? '' : '/index.js' %>'
3
+ export * as roles from './roles<%= ctx.isTypeScript ? '' : '/index.js' %>'
4
+ <% for (const modelScope of ctx.modelScopes) { -%>
5
+ export * as <%= modelScope.varName %>Models from './models/<%= modelScope.fileName %><%= ctx.isTypeScript ? '' : '/index.js' %>'
6
+ <% } -%>
@@ -0,0 +1,3 @@
1
+ <% for (const model of ctx.models) { -%>
2
+ export * from './<%= model.fileName %><%= ctx.isTypeScript ? '' : '.js' %>'
3
+ <% } -%>
@@ -0,0 +1,41 @@
1
+ <% if (ctx.isTypeScript) { -%>
2
+ <% for (const modelImport of ctx.imports) { -%>
3
+ import type { <%= modelImport.className %> } from './<%= modelImport.fileName %>'
4
+ <% } -%>
5
+ <% if (ctx.imports.length > 0) { -%>
6
+
7
+ <% } -%>
8
+ export interface <%= ctx.className %>Init {
9
+ <% for (const property of ctx.properties) { -%>
10
+ <%= property.name %><%= property.optional ? '?' : '' %>: <%= property.typeName %>
11
+ <% } -%>
12
+ }
13
+
14
+ export class <%= ctx.className %> implements <%= ctx.className %>Init {
15
+ <% for (const property of ctx.properties) { -%>
16
+ readonly <%= property.name %><%= property.optional ? '?' : '' %>: <%= property.typeName %>
17
+ <% } -%>
18
+
19
+ constructor({
20
+ <% for (const property of ctx.properties) { -%>
21
+ <%= property.name %>,
22
+ <% } -%>
23
+ }: <%= ctx.className %>Init = {} as <%= ctx.className %>Init) {
24
+ <% for (const property of ctx.properties) { -%>
25
+ this.<%= property.name %> = <%= property.name %>
26
+ <% } -%>
27
+ }
28
+ }
29
+ <% } else { -%>
30
+ export class <%= ctx.className %> {
31
+ constructor({
32
+ <% for (const property of ctx.properties) { -%>
33
+ <%= property.name %>,
34
+ <% } -%>
35
+ } = {}) {
36
+ <% for (const property of ctx.properties) { -%>
37
+ this.<%= property.name %> = <%= property.name %>
38
+ <% } -%>
39
+ }
40
+ }
41
+ <% } -%>