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