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