lombok-typescript 0.7.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
- [![version](https://img.shields.io/badge/npm-0.7.0-CB3837?logo=npm&label=version)](https://www.npmjs.com/package/lombok-typescript/v/0.7.0)
6
- [![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-6.0+-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
- [![Node.js](https://img.shields.io/badge/Node.js-22+-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.7.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,12 +69,17 @@ npm install lombok-typescript@preview
62
69
  - @Observable
63
70
  - @ChainOfResponsibility
64
71
  - @Iterable
72
+ - @Visitor
73
+ - @Visitable
74
+ - @Hook
65
75
 
66
76
  ### Structural patterns
67
77
 
68
78
  - @Flyweight
69
79
  - @Proxy
70
80
  - @Composite
81
+ - @Wraps
82
+ - @TemplateMethod
71
83
 
72
84
  ### TypeScript utilities
73
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")) {
@@ -371,6 +439,17 @@ function emitDeclarationModuleBlock(relSource, classes) {
371
439
  augments.push(" traverse(callback: (node: object) => void): void;");
372
440
  augments.push(" [Symbol.iterator](): IterableIterator<object>;");
373
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
+ }
374
453
  if (augments.length > 0) {
375
454
  lines.push(` interface ${info.name} {`);
376
455
  lines.push(...augments);
@@ -470,13 +549,68 @@ function emitWithFns(info) {
470
549
  return fields.map((f) => emitSingleWithFn(info, f)).join("\n\n");
471
550
  }
472
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
+
473
593
  // src/codegen/emitters/index.ts
474
594
  function emitImports(classes, importPath) {
475
595
  const names = classes.filter(hasCodegenClassDecorator).map((c) => c.name);
476
- if (names.length === 0) return "";
477
- return `import { ${names.join(", ")} } from '${importPath}';
478
-
479
- `;
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";
480
614
  }
481
615
  function emitToStringFn(info) {
482
616
  if (!wantsToString(info)) return "";
@@ -515,6 +649,10 @@ function emitApplyMixin(info) {
515
649
  `(ctor as typeof ${info.name} & { equals(a: ${info.name} | null | undefined, b: ${info.name} | null | undefined): boolean }).equals = ${info.name}_equalsStatic;`
516
650
  );
517
651
  }
652
+ const templateApply = emitTemplateMethodApplyAssignment(info);
653
+ if (templateApply) assignments.push(templateApply);
654
+ const visitableApply = emitVisitableAcceptApplyAssignment(info);
655
+ if (visitableApply) assignments.push(visitableApply);
518
656
  if (assignments.length === 0) return "";
519
657
  return `
520
658
  export function apply${info.name}Generated(ctor: typeof ${info.name}): void {
@@ -548,6 +686,12 @@ function emitClassCompanionBlocks(info) {
548
686
  if (delegate) blocks.push(delegate);
549
687
  const builderFn = emitBuilderFn(info);
550
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);
551
695
  const apply = emitApplyMixin(info);
552
696
  if (apply) blocks.push(apply);
553
697
  return blocks.filter(Boolean).join("\n\n");