lombok-typescript 0.1.0 → 0.2.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.
@@ -69,31 +69,190 @@ function toParameterInfo(param) {
69
69
  decorators: param.getDecorators().map(toDecoratorInfo)
70
70
  };
71
71
  }
72
+
73
+ // src/decorators/shared/composition.ts
74
+ var CONFLICTING_CLASS_DECORATOR_PAIRS = [
75
+ ["Data", "Value"]
76
+ ];
77
+ function classHasDecorator(info, name) {
78
+ return info.decorators.some((d) => d.name === name);
79
+ }
80
+ function validateClassComposition(info) {
81
+ for (const [a, b] of CONFLICTING_CLASS_DECORATOR_PAIRS) {
82
+ if (classHasDecorator(info, a) && classHasDecorator(info, b)) {
83
+ throw new Error(`Class "${info.name}": @${a} and @${b} cannot be used together.`);
84
+ }
85
+ }
86
+ }
87
+ function validateAllClassCompositions(classes) {
88
+ for (const info of classes) {
89
+ validateClassComposition(info);
90
+ }
91
+ }
72
92
  function toImportPath(sourcePath, fromDir) {
73
93
  let rel = relative(fromDir, sourcePath).replace(/\\/g, "/");
74
94
  if (!rel.startsWith(".")) rel = "./" + rel;
75
95
  return rel.replace(/\.tsx?$/u, ".js");
76
96
  }
97
+ var CODEGEN_CLASS_DECORATORS = [
98
+ "Builder",
99
+ "Data",
100
+ "ToString",
101
+ "Value",
102
+ "Equals",
103
+ "With"
104
+ ];
77
105
  function hasCodegenClassDecorator(info) {
78
- return hasClassDecorator(info, "Builder") || hasClassDecorator(info, "Data") || hasClassDecorator(info, "ToString");
106
+ return CODEGEN_CLASS_DECORATORS.some((name) => hasClassDecorator(info, name)) || info.fields.some(
107
+ (f) => fieldHasDecorator(f, "Getter") || fieldHasDecorator(f, "Setter") || fieldHasDecorator(f, "With") || fieldHasDecorator(f, "Delegate")
108
+ );
79
109
  }
80
110
  function hasClassDecorator(info, name) {
81
111
  return info.decorators.some((d) => d.name === name);
82
112
  }
113
+ function fieldHasDecorator(field, name) {
114
+ return field.decorators.some((d) => d.name === name);
115
+ }
83
116
  function fieldExcludesToString(field) {
84
117
  return field.decorators.some(
85
118
  (d) => d.name === "ToStringExclude" || d.name === "ToString.Exclude"
86
119
  );
87
120
  }
121
+ function fieldExcludesEquals(field) {
122
+ return field.decorators.some((d) => d.name === "EqualsExclude" || d.name === "Equals.Exclude");
123
+ }
88
124
  function visibleFields(info) {
89
- if (hasClassDecorator(info, "ToString")) {
125
+ if (hasClassDecorator(info, "ToString") || hasClassDecorator(info, "Data") || hasClassDecorator(info, "Value")) {
90
126
  return info.fields.filter((f) => !fieldExcludesToString(f));
91
127
  }
92
128
  return info.fields;
93
129
  }
130
+ function equalsFields(info) {
131
+ return info.fields.filter((f) => !fieldExcludesEquals(f));
132
+ }
133
+ function getterName(fieldName) {
134
+ return `get${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
135
+ }
136
+ function setterName(fieldName) {
137
+ return `set${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
138
+ }
139
+ function withMethodName(fieldName) {
140
+ return `with${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
141
+ }
94
142
  function builderClassName(className) {
95
143
  return `${className}Builder`;
96
144
  }
145
+ function hasDataOrValue(info) {
146
+ return hasClassDecorator(info, "Data") || hasClassDecorator(info, "Value");
147
+ }
148
+ function wantsWithMethods(info, field) {
149
+ return hasClassDecorator(info, "Value") || hasClassDecorator(info, "With") || fieldHasDecorator(field, "With");
150
+ }
151
+ function wantsGetter(info, field) {
152
+ return hasDataOrValue(info) || fieldHasDecorator(field, "Getter");
153
+ }
154
+ function wantsSetter(info, field) {
155
+ if (hasClassDecorator(info, "Value")) return false;
156
+ if (hasClassDecorator(info, "Data")) return !effectiveReadonly(info, field);
157
+ return fieldHasDecorator(field, "Setter");
158
+ }
159
+ function wantsEquals(info) {
160
+ return hasClassDecorator(info, "Data") || hasClassDecorator(info, "Value") || hasClassDecorator(info, "Equals");
161
+ }
162
+ function wantsToString(info) {
163
+ return hasClassDecorator(info, "ToString") || hasClassDecorator(info, "Data") || hasClassDecorator(info, "Value");
164
+ }
165
+ function effectiveReadonly(info, field) {
166
+ if (field.isReadonly) return true;
167
+ const defaults = getFieldDefaultsOptions(info);
168
+ return defaults?.makeFinal === true;
169
+ }
170
+ function getFieldDefaultsOptions(info) {
171
+ const dec = info.decorators.find((d) => d.name === "FieldDefaults");
172
+ if (!dec) return void 0;
173
+ const [first] = dec.arguments;
174
+ if (typeof first === "string" && first.startsWith("{")) {
175
+ try {
176
+ const parsed = JSON.parse(first.replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
177
+ return {
178
+ level: parsed.level ?? "public",
179
+ makeFinal: parsed.makeFinal ?? false
180
+ };
181
+ } catch {
182
+ return { level: "public", makeFinal: false };
183
+ }
184
+ }
185
+ return { level: "public", makeFinal: false };
186
+ }
187
+ function hasFluentAccessors(info) {
188
+ const dec = info.decorators.find((d) => d.name === "Accessors");
189
+ if (!dec) return false;
190
+ const [first] = dec.arguments;
191
+ if (typeof first === "string") {
192
+ return first.includes("chain") || first.includes("fluent");
193
+ }
194
+ return false;
195
+ }
196
+ function getDelegateMethods(field) {
197
+ const dec = field.decorators.find((d) => d.name === "Delegate");
198
+ if (!dec || dec.arguments.length === 0) return [];
199
+ if (dec.arguments.length === 1 && String(dec.arguments[0]).startsWith("[")) {
200
+ try {
201
+ return JSON.parse(String(dec.arguments[0]).replace(/'/g, '"'));
202
+ } catch {
203
+ return [];
204
+ }
205
+ }
206
+ return dec.arguments.map((a) => String(a).replace(/^['"]|['"]$/g, ""));
207
+ }
208
+
209
+ // src/codegen/emitters/accessors-emit.ts
210
+ function emitGetterFn(info, field) {
211
+ const g = getterName(field.name);
212
+ return `
213
+ function ${info.name}_${g}(this: ${info.name}): ${field.type} {
214
+ return this.${field.name};
215
+ }`.trim();
216
+ }
217
+ function emitSetterFn(info, field) {
218
+ const s = setterName(field.name);
219
+ const fluent = hasFluentAccessors(info);
220
+ const ret = fluent ? `return this;` : "";
221
+ const returnType = fluent ? info.name : "void";
222
+ return `
223
+ function ${info.name}_${s}(this: ${info.name}, value: ${field.type}): ${returnType} {
224
+ this.${field.name} = value;
225
+ ${ret}
226
+ }`.trim();
227
+ }
228
+ function emitAccessorFns(info) {
229
+ const fns = [];
230
+ for (const field of info.fields) {
231
+ if (wantsGetter(info, field)) {
232
+ fns.push(emitGetterFn(info, field));
233
+ }
234
+ if (wantsSetter(info, field)) {
235
+ fns.push(emitSetterFn(info, field));
236
+ }
237
+ }
238
+ return fns.join("\n\n");
239
+ }
240
+ function emitAccessorApplyAssignments(info) {
241
+ const assignments = [];
242
+ for (const field of info.fields) {
243
+ if (wantsGetter(info, field)) {
244
+ assignments.push(
245
+ `prototype.${getterName(field.name)} = ${info.name}_${getterName(field.name)};`
246
+ );
247
+ }
248
+ if (wantsSetter(info, field)) {
249
+ assignments.push(
250
+ `prototype.${setterName(field.name)} = ${info.name}_${setterName(field.name)};`
251
+ );
252
+ }
253
+ }
254
+ return assignments;
255
+ }
97
256
 
98
257
  // src/codegen/emitters/builder.ts
99
258
  function emitBuilderClass(info) {
@@ -131,16 +290,8 @@ ${info.fields.map((f) => ` instance.${f.name} = this._${f.name}${f.isOptional
131
290
  }
132
291
  }`.trim();
133
292
  }
134
- function emitDeclarationShim(sourcePath, companionOutputPath, classes) {
135
- const relSource = toImportPath(sourcePath, dirname(companionOutputPath));
136
- const lines = [
137
- "// Auto-generated type augmentation by lombok-typescript.",
138
- "// Do not edit. Regenerate via `lombok-ts generate`.",
139
- "",
140
- "export {};",
141
- "",
142
- `declare module '${relSource}' {`
143
- ];
293
+ function emitDeclarationModuleBlock(relSource, classes) {
294
+ const lines = [`declare module '${relSource}' {`];
144
295
  for (const info of classes) {
145
296
  if (hasClassDecorator(info, "Builder")) {
146
297
  const builderName = builderClassName(info.name);
@@ -155,24 +306,33 @@ function emitDeclarationShim(sourcePath, companionOutputPath, classes) {
155
306
  }
156
307
  if (hasClassDecorator(info, "Builder")) {
157
308
  const builderName = builderClassName(info.name);
158
- lines.push(` export class ${info.name} {`);
159
- lines.push(` static builder(): ${builderName};`);
309
+ lines.push(` namespace ${info.name} {`);
310
+ lines.push(` export function builder(): ${builderName};`);
160
311
  lines.push(" }");
161
312
  lines.push("");
162
313
  }
163
314
  const augments = [];
164
- if (hasClassDecorator(info, "Data")) {
165
- for (const f of info.fields) {
166
- const g = `get${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
167
- augments.push(` ${g}(): ${f.type};`);
168
- if (!f.isReadonly) {
169
- const s = `set${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
170
- augments.push(` ${s}(value: ${f.type}): void;`);
315
+ for (const f of info.fields) {
316
+ if (wantsGetter(info, f)) {
317
+ augments.push(` ${getterName(f.name)}(): ${f.type};`);
318
+ }
319
+ if (wantsSetter(info, f)) {
320
+ const ret = hasClassDecorator(info, "Accessors") ? info.name : "void";
321
+ augments.push(` ${setterName(f.name)}(value: ${f.type}): ${ret};`);
322
+ }
323
+ if (wantsWithMethods(info, f)) {
324
+ augments.push(` ${withMethodName(f.name)}(value: ${f.type}): ${info.name};`);
325
+ }
326
+ if (fieldHasDecorator(f, "Delegate")) {
327
+ for (const method of getDelegateMethods(f)) {
328
+ augments.push(` ${method}(...args: unknown[]): unknown;`);
171
329
  }
172
330
  }
331
+ }
332
+ if (wantsEquals(info)) {
173
333
  augments.push(` equals(other: ${info.name} | null | undefined): boolean;`);
174
- augments.push(` toString(): string;`);
175
- } else if (hasClassDecorator(info, "ToString")) {
334
+ }
335
+ if (wantsToString(info)) {
176
336
  augments.push(" toString(): string;");
177
337
  }
178
338
  if (augments.length > 0) {
@@ -180,11 +340,99 @@ function emitDeclarationShim(sourcePath, companionOutputPath, classes) {
180
340
  lines.push(...augments);
181
341
  lines.push(" }");
182
342
  }
343
+ if (hasClassDecorator(info, "Equals")) {
344
+ lines.push(` namespace ${info.name} {`);
345
+ lines.push(
346
+ ` export function equals(a: ${info.name} | null | undefined, b: ${info.name} | null | undefined): boolean;`
347
+ );
348
+ lines.push(" }");
349
+ lines.push("");
350
+ }
183
351
  }
184
352
  lines.push("}");
185
- lines.push("");
186
353
  return lines.join("\n");
187
354
  }
355
+ function emitDeclarationShim(sourcePath, companionOutputPath, classes) {
356
+ const relSource = toImportPath(sourcePath, dirname(companionOutputPath));
357
+ const moduleBlock = emitDeclarationModuleBlock(relSource, classes);
358
+ return [
359
+ "// Auto-generated type augmentation by lombok-typescript.",
360
+ "// Do not edit. Regenerate via `lombok-ts generate`.",
361
+ "",
362
+ "export {};",
363
+ "",
364
+ moduleBlock,
365
+ ""
366
+ ].join("\n");
367
+ }
368
+
369
+ // src/codegen/emitters/delegate-emit.ts
370
+ function emitDelegateFns(info) {
371
+ const fns = [];
372
+ for (const field of info.fields) {
373
+ if (!fieldHasDecorator(field, "Delegate")) continue;
374
+ const methods = getDelegateMethods(field);
375
+ for (const method of methods) {
376
+ fns.push(
377
+ `
378
+ function ${info.name}_${method}(this: ${info.name}, ...args: unknown[]): unknown {
379
+ const target = this.${field.name} as { ${method}: (...a: unknown[]) => unknown };
380
+ return target.${method}(...args);
381
+ }`.trim()
382
+ );
383
+ }
384
+ }
385
+ return fns.join("\n\n");
386
+ }
387
+ function emitDelegateApplyAssignments(info) {
388
+ const assignments = [];
389
+ for (const field of info.fields) {
390
+ if (!fieldHasDecorator(field, "Delegate")) continue;
391
+ for (const method of getDelegateMethods(field)) {
392
+ assignments.push(`prototype.${method} = ${info.name}_${method};`);
393
+ }
394
+ }
395
+ return assignments;
396
+ }
397
+
398
+ // src/codegen/emitters/equals-emit.ts
399
+ function emitEqualsFn(info) {
400
+ if (!wantsEquals(info)) return "";
401
+ const fields = equalsFields(info);
402
+ const checks = fields.map((f) => `this.${f.name} === other.${f.name}`).join(" &&\n ");
403
+ return `
404
+ function ${info.name}_equals(this: ${info.name}, other: ${info.name} | null | undefined): boolean {
405
+ if (other === null || other === undefined) return false;
406
+ if (!(other instanceof (this.constructor as typeof ${info.name}))) return false;
407
+ return ${checks || "true"};
408
+ }`.trim();
409
+ }
410
+ function emitEqualsStaticFn(info) {
411
+ if (!hasClassDecorator(info, "Equals")) return "";
412
+ return `
413
+ function ${info.name}_equalsStatic(a: ${info.name} | null | undefined, b: ${info.name} | null | undefined): boolean {
414
+ if (a === b) return true;
415
+ if (a === null || a === undefined || b === null || b === undefined) return false;
416
+ return a.equals(b);
417
+ }`.trim();
418
+ }
419
+
420
+ // src/codegen/emitters/with-emit.ts
421
+ function emitSingleWithFn(info, field) {
422
+ const method = withMethodName(field.name);
423
+ return `
424
+ function ${info.name}_${method}(this: ${info.name}, value: ${field.type}): ${info.name} {
425
+ const copy = Object.create(Object.getPrototypeOf(this)) as ${info.name};
426
+ Object.assign(copy, this);
427
+ copy.${field.name} = value;
428
+ return copy;
429
+ }`.trim();
430
+ }
431
+ function emitWithFns(info) {
432
+ const fields = info.fields.filter((f) => wantsWithMethods(info, f));
433
+ if (fields.length === 0) return "";
434
+ return fields.map((f) => emitSingleWithFn(info, f)).join("\n\n");
435
+ }
188
436
 
189
437
  // src/codegen/emitters/index.ts
190
438
  function emitImports(classes, importPath) {
@@ -195,7 +443,7 @@ function emitImports(classes, importPath) {
195
443
  `;
196
444
  }
197
445
  function emitToStringFn(info) {
198
- if (!hasClassDecorator(info, "ToString") && !hasClassDecorator(info, "Data")) return "";
446
+ if (!wantsToString(info)) return "";
199
447
  const fields = visibleFields(info);
200
448
  const parts = fields.map((f) => `${f.name}=\${String(this.${f.name})}`).join(", ");
201
449
  return `
@@ -205,26 +453,32 @@ function ${info.name}_toString(this: ${info.name}): string {
205
453
  }
206
454
  function emitApplyMixin(info) {
207
455
  const assignments = [];
208
- if (hasClassDecorator(info, "Data")) {
209
- for (const f of info.fields) {
210
- const g = `get${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
211
- assignments.push(`prototype.${g} = ${info.name}_${g};`);
212
- if (!f.isReadonly) {
213
- const s = `set${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
214
- assignments.push(`prototype.${s} = ${info.name}_${s};`);
215
- }
216
- }
456
+ assignments.push(...emitAccessorApplyAssignments(info));
457
+ assignments.push(...emitDelegateApplyAssignments(info));
458
+ if (wantsEquals(info)) {
217
459
  assignments.push(`prototype.equals = ${info.name}_equals;`);
218
- assignments.push(`prototype.toString = ${info.name}_toString;`);
219
- } else if (hasClassDecorator(info, "ToString")) {
460
+ }
461
+ if (wantsToString(info)) {
220
462
  assignments.push(`prototype.toString = ${info.name}_toString;`);
221
463
  }
464
+ for (const field of info.fields) {
465
+ if (wantsWithMethods(info, field)) {
466
+ assignments.push(
467
+ `prototype.${withMethodName(field.name)} = ${info.name}_${withMethodName(field.name)};`
468
+ );
469
+ }
470
+ }
222
471
  if (hasClassDecorator(info, "Builder")) {
223
472
  const bName = builderClassName(info.name);
224
473
  assignments.push(
225
474
  `(ctor as typeof ${info.name} & { builder(): ${bName} }).builder = ${info.name}_builder;`
226
475
  );
227
476
  }
477
+ if (hasClassDecorator(info, "Equals")) {
478
+ assignments.push(
479
+ `(ctor as typeof ${info.name} & { equals(a: ${info.name} | null | undefined, b: ${info.name} | null | undefined): boolean }).equals = ${info.name}_equalsStatic;`
480
+ );
481
+ }
228
482
  if (assignments.length === 0) return "";
229
483
  return `
230
484
  export function apply${info.name}Generated(ctor: typeof ${info.name}): void {
@@ -232,39 +486,6 @@ export function apply${info.name}Generated(ctor: typeof ${info.name}): void {
232
486
  ${assignments.join("\n ")}
233
487
  }`.trim();
234
488
  }
235
- function emitDataFns(info) {
236
- if (!hasClassDecorator(info, "Data")) return "";
237
- const fns = [];
238
- for (const f of info.fields) {
239
- const g = `get${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
240
- fns.push(
241
- `
242
- function ${info.name}_${g}(this: ${info.name}): ${f.type} {
243
- return this.${f.name};
244
- }`.trim()
245
- );
246
- if (!f.isReadonly) {
247
- const s = `set${f.name.charAt(0).toUpperCase()}${f.name.slice(1)}`;
248
- fns.push(
249
- `
250
- function ${info.name}_${s}(this: ${info.name}, value: ${f.type}): void {
251
- this.${f.name} = value;
252
- }`.trim()
253
- );
254
- }
255
- }
256
- const equalsBody = info.fields.map((f) => `this.${f.name} === other.${f.name}`).join(" &&\n ");
257
- fns.push(
258
- `
259
- function ${info.name}_equals(this: ${info.name}, other: ${info.name} | null | undefined): boolean {
260
- if (other === null || other === undefined) return false;
261
- if (!(other instanceof (this.constructor as typeof ${info.name}))) return false;
262
- return ${equalsBody || "true"};
263
- }`.trim()
264
- );
265
- fns.push(emitToStringFn(info));
266
- return fns.filter(Boolean).join("\n\n");
267
- }
268
489
  function emitBuilderFn(info) {
269
490
  if (!hasClassDecorator(info, "Builder")) return "";
270
491
  const bName = builderClassName(info.name);
@@ -273,7 +494,30 @@ function ${info.name}_builder(): ${bName} {
273
494
  return ${bName}.builder();
274
495
  }`.trim();
275
496
  }
497
+ function emitClassCompanionBlocks(info) {
498
+ const blocks = [];
499
+ const builder = emitBuilderClass(info);
500
+ if (builder) blocks.push(builder);
501
+ const accessors = emitAccessorFns(info);
502
+ if (accessors) blocks.push(accessors);
503
+ const withFns = emitWithFns(info);
504
+ if (withFns) blocks.push(withFns);
505
+ const equalsFn = emitEqualsFn(info);
506
+ if (equalsFn) blocks.push(equalsFn);
507
+ const equalsStatic = emitEqualsStaticFn(info);
508
+ if (equalsStatic) blocks.push(equalsStatic);
509
+ const toString = emitToStringFn(info);
510
+ if (toString) blocks.push(toString);
511
+ const delegate = emitDelegateFns(info);
512
+ if (delegate) blocks.push(delegate);
513
+ const builderFn = emitBuilderFn(info);
514
+ if (builderFn) blocks.push(builderFn);
515
+ const apply = emitApplyMixin(info);
516
+ if (apply) blocks.push(apply);
517
+ return blocks.filter(Boolean).join("\n\n");
518
+ }
276
519
  function emitCompanionFile(sourcePath, companionOutputPath, classes, cwd) {
520
+ validateAllClassCompositions(classes);
277
521
  const header = [
278
522
  "// Auto-generated by lombok-typescript.",
279
523
  "// Source: " + relative(cwd, sourcePath).replace(/\\/g, "/"),
@@ -283,25 +527,16 @@ function emitCompanionFile(sourcePath, companionOutputPath, classes, cwd) {
283
527
  const importPath = toImportPath(sourcePath, dirname(companionOutputPath));
284
528
  const blocks = [];
285
529
  for (const info of classes) {
286
- const builder = emitBuilderClass(info);
287
- if (builder) blocks.push(builder);
288
- if (hasClassDecorator(info, "Data")) {
289
- blocks.push(emitDataFns(info));
290
- } else {
291
- const ts2 = emitToStringFn(info);
292
- if (ts2) blocks.push(ts2);
293
- }
294
- const builderFn = emitBuilderFn(info);
295
- if (builderFn) blocks.push(builderFn);
296
- const apply = emitApplyMixin(info);
297
- if (apply) blocks.push(apply);
530
+ const chunk = emitClassCompanionBlocks(info);
531
+ if (chunk) blocks.push(chunk);
298
532
  }
299
- const applyAll = classes.filter((c) => emitApplyMixin(c)).length > 0 ? `
533
+ const classesWithApply = classes.filter((c) => emitApplyMixin(c).length > 0);
534
+ const applyAll = classesWithApply.length > 0 ? `
300
535
 
301
536
  export function applyAllGenerated(handlers: {
302
- ${classes.filter((c) => emitApplyMixin(c)).map((c) => ` ${c.name}: typeof ${c.name};`).join("\n")}
537
+ ${classesWithApply.map((c) => ` ${c.name}: typeof ${c.name};`).join("\n")}
303
538
  }): void {
304
- ${classes.filter((c) => emitApplyMixin(c)).map((c) => ` apply${c.name}Generated(handlers.${c.name});`).join("\n")}
539
+ ${classesWithApply.map((c) => ` apply${c.name}Generated(handlers.${c.name});`).join("\n")}
305
540
  }
306
541
  ` : "\nexport {};\n";
307
542
  const imports = emitImports(classes, importPath);
@@ -343,7 +578,7 @@ var CodeGenerator = class {
343
578
  const { ts, dts } = emitCompanionFile(sourcePath, outputPath, classes, process.cwd());
344
579
  const content = ts;
345
580
  this.writeOutput(outputPath, content);
346
- this.writeOutput(outputPath.replace(/\.lombok\.ts$/u, ".lombok.d.ts"), dts);
581
+ this.writeOutput(outputPath.replace(/\.lombok\.ts$/u, ".lombok.augment.d.ts"), dts);
347
582
  generated.push({
348
583
  sourcePath,
349
584
  outputPath,
@@ -366,7 +601,7 @@ var CodeGenerator = class {
366
601
  const { ts, dts } = emitCompanionFile(filePath, outputPath, classes, process.cwd());
367
602
  const content = ts;
368
603
  this.writeOutput(outputPath, content);
369
- this.writeOutput(outputPath.replace(/\.lombok\.ts$/u, ".lombok.d.ts"), dts);
604
+ this.writeOutput(outputPath.replace(/\.lombok\.ts$/u, ".lombok.augment.d.ts"), dts);
370
605
  return {
371
606
  sourcePath: filePath,
372
607
  outputPath,
@@ -374,13 +609,57 @@ var CodeGenerator = class {
374
609
  processedClasses: classes.map((c) => c.name)
375
610
  };
376
611
  }
377
- /** Watch mode is not implemented yet. Re-run `lombok-ts generate` for now. */
378
- async watch() {
379
- throw new Error(
380
- "Watch mode is not implemented yet. Re-run `lombok-ts generate` after changes."
381
- );
612
+ /** Watch for source changes and regenerate companion files. */
613
+ async watch(options = {}) {
614
+ const log = options.log ?? ((msg) => console.info(msg));
615
+ const generated = await this.generate();
616
+ log(`Generated ${generated.length} companion file(s). Watching for changes\u2026`);
617
+ if (options.signal?.aborted) return;
618
+ const { watch } = await import('fs');
619
+ const watchers = [];
620
+ const debounceTimers = /* @__PURE__ */ new Map();
621
+ const project = this.createProject();
622
+ const paths = project.getSourceFiles().map((sf) => sf.getFilePath()).filter((p) => this.shouldProcess(p));
623
+ const scheduleRegenerate = (filePath) => {
624
+ const existing = debounceTimers.get(filePath);
625
+ if (existing) clearTimeout(existing);
626
+ debounceTimers.set(
627
+ filePath,
628
+ setTimeout(() => {
629
+ void this.generateForFile(filePath).then((result) => {
630
+ if (result) {
631
+ log(`Regenerated ${relative(process.cwd(), result.outputPath)}`);
632
+ }
633
+ });
634
+ }, 100)
635
+ );
636
+ };
637
+ for (const filePath of paths) {
638
+ try {
639
+ const watcher = watch(filePath, { persistent: true }, (event) => {
640
+ if (event === "change") scheduleRegenerate(filePath);
641
+ });
642
+ watchers.push(watcher);
643
+ } catch {
644
+ }
645
+ }
646
+ await new Promise((resolve2) => {
647
+ const onAbort = () => {
648
+ for (const w of watchers) w.close();
649
+ for (const t of debounceTimers.values()) clearTimeout(t);
650
+ resolve2();
651
+ };
652
+ if (options.signal) {
653
+ if (options.signal.aborted) {
654
+ onAbort();
655
+ return;
656
+ }
657
+ options.signal.addEventListener("abort", onAbort, { once: true });
658
+ return;
659
+ }
660
+ });
382
661
  }
383
- // Internal helpers
662
+ // Internal helpers — previously threw for watch stub
384
663
  createProject() {
385
664
  const tsConfig = resolve(this.options.tsConfigPath);
386
665
  if (existsSync(tsConfig)) {