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