lombok-typescript 0.6.0 → 0.8.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/README.md CHANGED
@@ -2,18 +2,24 @@
2
2
 
3
3
  TypeScript decorators inspired by Project Lombok and common design patterns. Supports legacy `experimentalDecorators` and Stage 3 decorators via separate entry points.
4
4
 
5
- [![npm latest](https://img.shields.io/npm/v/lombok-typescript/latest?label=latest&logo=npm&color=CB3837)](https://www.npmjs.com/package/lombok-typescript)
6
- [![npm preview](https://img.shields.io/npm/v/lombok-typescript/preview?label=preview&logo=npm&color=CB3837)](https://www.npmjs.com/package/lombok-typescript/v/preview)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-%E2%89%A56.0-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
- [![Node](https://img.shields.io/badge/node-%E2%89%A522-brightgreen&logo=node.js&logoColor=white)](https://nodejs.org/)
9
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![version](https://img.shields.io/badge/npm-0.8.0-007ec6?logo=npm&label=version)](https://www.npmjs.com/package/lombok-typescript/v/0.8.0)
6
+ [![preview](https://img.shields.io/npm/v/lombok-typescript/preview?label=preview&logo=npm&color=007ec6)](https://www.npmjs.com/package/lombok-typescript/v/preview)
7
+
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0+-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-22+-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
10
+ [![License](https://img.shields.io/badge/License-MIT-007ec6.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/lombok-typescript@0.8.0?label=bundle%20size&color=007ec6)](https://bundlephobia.com/package/lombok-typescript@0.8.0)
13
+ [![deps.dev](https://img.shields.io/badge/deps.dev-package-007ec6?logo=googlechrome)](https://deps.dev/npm/lombok-typescript/0.8.0)
14
+ [![Socket](https://img.shields.io/badge/Socket-Supply%20Chain%2078-007ec6?logo=socket)](https://socket.dev/npm/package/lombok-typescript)
15
+ [![Snyk](https://img.shields.io/snyk/vulnerabilities/npm/lombok-typescript?label=snyk&color=007ec6)](https://snyk.io/advisor/npm-package/lombok-typescript)
10
16
 
11
17
  ## Install
12
18
 
13
19
  Install this release:
14
20
 
15
21
  ```bash
16
- npm install lombok-typescript@0.6.0
22
+ npm install lombok-typescript@0.8.0
17
23
  ```
18
24
 
19
25
  Install the current `latest` tag (stable release):
@@ -52,6 +58,7 @@ npm install lombok-typescript@preview
52
58
  - @Singleton
53
59
  - @Prototype
54
60
  - @Factory
61
+ - @AbstractFactory
55
62
 
56
63
  ### Behavioral patterns
57
64
 
@@ -62,6 +69,17 @@ npm install lombok-typescript@preview
62
69
  - @Observable
63
70
  - @ChainOfResponsibility
64
71
  - @Iterable
72
+ - @Visitor
73
+ - @Visitable
74
+ - @Hook
75
+
76
+ ### Structural patterns
77
+
78
+ - @Flyweight
79
+ - @Proxy
80
+ - @Composite
81
+ - @Wraps
82
+ - @TemplateMethod
65
83
 
66
84
  ### TypeScript utilities
67
85
 
@@ -110,7 +110,10 @@ var CODEGEN_CLASS_DECORATORS = [
110
110
  "ToString",
111
111
  "Value",
112
112
  "Equals",
113
- "With"
113
+ "With",
114
+ "TemplateMethod",
115
+ "AbstractFactory",
116
+ "Visitable"
114
117
  ];
115
118
  function hasCodegenClassDecorator(info) {
116
119
  return CODEGEN_CLASS_DECORATORS.some((name) => hasClassDecorator(info, name)) || info.fields.some(
@@ -215,6 +218,54 @@ function getDelegateMethods(field) {
215
218
  }
216
219
  return dec.arguments.map((a) => String(a).replace(/^['"]|['"]$/g, ""));
217
220
  }
221
+ function parseDecoratorObjectArg(dec) {
222
+ if (!dec || dec.arguments.length === 0) return {};
223
+ const [first] = dec.arguments;
224
+ if (typeof first !== "string" || !first.startsWith("{")) return {};
225
+ try {
226
+ return JSON.parse(first.replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
227
+ } catch {
228
+ return {};
229
+ }
230
+ }
231
+ function parseDecoratorArrayArg(dec) {
232
+ if (!dec || dec.arguments.length === 0) return [];
233
+ const [first] = dec.arguments;
234
+ const text = String(first);
235
+ if (!text.startsWith("[")) return [];
236
+ try {
237
+ return JSON.parse(text.replace(/'/g, '"'));
238
+ } catch {
239
+ return [];
240
+ }
241
+ }
242
+ function getAbstractFactoryProducts(info) {
243
+ const dec = info.decorators.find((d) => d.name === "AbstractFactory");
244
+ const fromArray = parseDecoratorArrayArg(dec);
245
+ if (fromArray.length > 0) return fromArray;
246
+ const obj = parseDecoratorObjectArg(dec);
247
+ return Array.isArray(obj.products) ? obj.products : [];
248
+ }
249
+ function getTemplateMethodSteps(info) {
250
+ const dec = info.decorators.find((d) => d.name === "TemplateMethod");
251
+ const obj = parseDecoratorObjectArg(dec);
252
+ return Array.isArray(obj.steps) ? obj.steps : [];
253
+ }
254
+ function getTemplateMethodName(info) {
255
+ const dec = info.decorators.find((d) => d.name === "TemplateMethod");
256
+ const obj = parseDecoratorObjectArg(dec);
257
+ return typeof obj.template === "string" ? obj.template : "execute";
258
+ }
259
+ function getHookMethodNames(info) {
260
+ return info.methods.filter((m) => m.decorators.some((d) => d.name === "Hook")).map((m) => {
261
+ const hookDec = m.decorators.find((d) => d.name === "Hook");
262
+ const opts = parseDecoratorObjectArg(hookDec);
263
+ return typeof opts.name === "string" ? opts.name : m.name;
264
+ });
265
+ }
266
+ function visitMethodName(className) {
267
+ return `visit${className}`;
268
+ }
218
269
 
219
270
  // src/codegen/emitters/accessors-emit.ts
220
271
  function emitGetterFn(info, field) {
@@ -264,6 +315,23 @@ function emitAccessorApplyAssignments(info) {
264
315
  return assignments;
265
316
  }
266
317
 
318
+ // src/codegen/emitters/abstract-factory-emit.ts
319
+ function emitAbstractFactoryMixin(info) {
320
+ if (!hasClassDecorator(info, "AbstractFactory")) return "";
321
+ const products = getAbstractFactoryProducts(info);
322
+ if (products.length === 0) {
323
+ throw new Error(`@AbstractFactory on ${info.name}: product list is empty`);
324
+ }
325
+ const methods = products.map((product) => {
326
+ const methodName = `create${product}`;
327
+ return ` abstract ${methodName}(): ${product};`;
328
+ }).join("\n");
329
+ return `
330
+ export abstract class ${info.name}Mixin {
331
+ ${methods}
332
+ }`.trim();
333
+ }
334
+
267
335
  // src/codegen/emitters/builder.ts
268
336
  function emitBuilderClass(info) {
269
337
  if (!hasClassDecorator(info, "Builder")) {
@@ -363,6 +431,25 @@ function emitDeclarationModuleBlock(relSource, classes) {
363
431
  if (hasClassDecorator(info, "Iterable")) {
364
432
  augments.push(" [Symbol.iterator](): IterableIterator<unknown>;");
365
433
  }
434
+ if (hasClassDecorator(info, "Composite")) {
435
+ augments.push(" add(child: object): void;");
436
+ augments.push(" remove(child: object): void;");
437
+ augments.push(" getChild(index: number): object;");
438
+ augments.push(" getChildren(): readonly object[];");
439
+ augments.push(" traverse(callback: (node: object) => void): void;");
440
+ augments.push(" [Symbol.iterator](): IterableIterator<object>;");
441
+ }
442
+ if (hasClassDecorator(info, "Wraps")) {
443
+ const dec = info.decorators.find((d) => d.name === "Wraps");
444
+ const innerName = dec?.arguments[0] ? String(dec.arguments[0]) : "unknown";
445
+ augments.push(` protected inner: ${innerName};`);
446
+ }
447
+ if (hasClassDecorator(info, "Visitable")) {
448
+ augments.push(" accept(visitor: unknown): unknown;");
449
+ }
450
+ if (hasClassDecorator(info, "TemplateMethod")) {
451
+ augments.push(` ${getTemplateMethodName(info)}(): void;`);
452
+ }
366
453
  if (augments.length > 0) {
367
454
  lines.push(` interface ${info.name} {`);
368
455
  lines.push(...augments);
@@ -462,13 +549,68 @@ function emitWithFns(info) {
462
549
  return fields.map((f) => emitSingleWithFn(info, f)).join("\n\n");
463
550
  }
464
551
 
552
+ // src/codegen/emitters/template-method-emit.ts
553
+ function emitTemplateMethodFn(info) {
554
+ if (!hasClassDecorator(info, "TemplateMethod")) return "";
555
+ const templateName = getTemplateMethodName(info);
556
+ const steps = getTemplateMethodSteps(info);
557
+ const hookNames = getHookMethodNames(info);
558
+ for (const step of steps) {
559
+ if (!hookNames.includes(step)) {
560
+ throw new Error(`@TemplateMethod on ${info.name}: missing @Hook method for step "${step}"`);
561
+ }
562
+ }
563
+ const calls = steps.map((step) => `this.${step}();`).join(" ");
564
+ return `
565
+ function ${info.name}_${templateName}(this: ${info.name}): void {
566
+ ${calls}
567
+ }`.trim();
568
+ }
569
+ function emitTemplateMethodApplyAssignment(info) {
570
+ if (!hasClassDecorator(info, "TemplateMethod")) return void 0;
571
+ const templateName = getTemplateMethodName(info);
572
+ return `prototype.${templateName} = ${info.name}_${templateName};`;
573
+ }
574
+
575
+ // src/codegen/emitters/visitor-emit.ts
576
+ function emitVisitableAcceptFn(info) {
577
+ if (!hasClassDecorator(info, "Visitable")) return "";
578
+ const methodName = visitMethodName(info.name);
579
+ return `
580
+ function ${info.name}_accept(this: ${info.name}, visitor: unknown): unknown {
581
+ const handler = (visitor as Record<string, unknown>)['${methodName}'];
582
+ if (typeof handler !== 'function') {
583
+ throw new Error(\`Visitor missing ${methodName} for ${info.name}\`);
584
+ }
585
+ return handler.call(visitor, this);
586
+ }`.trim();
587
+ }
588
+ function emitVisitableAcceptApplyAssignment(info) {
589
+ if (!hasClassDecorator(info, "Visitable")) return void 0;
590
+ return `prototype.accept = ${info.name}_accept;`;
591
+ }
592
+
465
593
  // src/codegen/emitters/index.ts
466
594
  function emitImports(classes, importPath) {
467
595
  const names = classes.filter(hasCodegenClassDecorator).map((c) => c.name);
468
- if (names.length === 0) return "";
469
- return `import { ${names.join(", ")} } from '${importPath}';
470
-
471
- `;
596
+ const productTypes = /* @__PURE__ */ new Set();
597
+ for (const info of classes) {
598
+ if (hasClassDecorator(info, "AbstractFactory")) {
599
+ for (const product of getAbstractFactoryProducts(info)) {
600
+ productTypes.add(product);
601
+ }
602
+ }
603
+ }
604
+ const importLines = [];
605
+ if (names.length > 0) {
606
+ importLines.push(`import { ${names.join(", ")} } from '${importPath}';`);
607
+ }
608
+ const extraProducts = [...productTypes].filter((p) => !names.includes(p));
609
+ if (extraProducts.length > 0) {
610
+ importLines.push(`import type { ${extraProducts.join(", ")} } from '${importPath}';`);
611
+ }
612
+ if (importLines.length === 0) return "";
613
+ return importLines.join("\n") + "\n\n";
472
614
  }
473
615
  function emitToStringFn(info) {
474
616
  if (!wantsToString(info)) return "";
@@ -507,6 +649,10 @@ function emitApplyMixin(info) {
507
649
  `(ctor as typeof ${info.name} & { equals(a: ${info.name} | null | undefined, b: ${info.name} | null | undefined): boolean }).equals = ${info.name}_equalsStatic;`
508
650
  );
509
651
  }
652
+ const templateApply = emitTemplateMethodApplyAssignment(info);
653
+ if (templateApply) assignments.push(templateApply);
654
+ const visitableApply = emitVisitableAcceptApplyAssignment(info);
655
+ if (visitableApply) assignments.push(visitableApply);
510
656
  if (assignments.length === 0) return "";
511
657
  return `
512
658
  export function apply${info.name}Generated(ctor: typeof ${info.name}): void {
@@ -540,6 +686,12 @@ function emitClassCompanionBlocks(info) {
540
686
  if (delegate) blocks.push(delegate);
541
687
  const builderFn = emitBuilderFn(info);
542
688
  if (builderFn) blocks.push(builderFn);
689
+ const templateMethod = emitTemplateMethodFn(info);
690
+ if (templateMethod) blocks.push(templateMethod);
691
+ const visitableAccept = emitVisitableAcceptFn(info);
692
+ if (visitableAccept) blocks.push(visitableAccept);
693
+ const abstractFactory = emitAbstractFactoryMixin(info);
694
+ if (abstractFactory) blocks.push(abstractFactory);
543
695
  const apply = emitApplyMixin(info);
544
696
  if (apply) blocks.push(apply);
545
697
  return blocks.filter(Boolean).join("\n\n");