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/README.md +33 -64
- package/dist/cli/index.cjs +389 -98
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +389 -98
- package/dist/cli/index.js.map +1 -1
- package/dist/codegen/index.cjs +371 -92
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +6 -3
- package/dist/codegen/index.d.ts +6 -3
- package/dist/codegen/index.js +371 -92
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy/index.cjs +195 -0
- package/dist/legacy/index.cjs.map +1 -1
- package/dist/legacy/index.d.cts +24 -3
- package/dist/legacy/index.d.ts +24 -3
- package/dist/legacy/index.js +185 -1
- package/dist/legacy/index.js.map +1 -1
- package/dist/{memoize-DvelzGDl.d.ts → log-Dbe0sSP1.d.ts} +23 -1
- package/dist/{memoize-Bj9pm_cK.d.cts → log-Dpr79VRt.d.cts} +23 -1
- package/dist/stage3/index.cjs +200 -0
- package/dist/stage3/index.cjs.map +1 -1
- package/dist/stage3/index.d.cts +24 -3
- package/dist/stage3/index.d.ts +24 -3
- package/dist/stage3/index.js +190 -1
- package/dist/stage3/index.js.map +1 -1
- package/package.json +4 -17
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
|
|
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
|
|
141
|
-
const
|
|
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(`
|
|
165
|
-
lines.push(`
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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 (!
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
}
|
|
226
|
-
|
|
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
|
|
293
|
-
if (
|
|
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
|
|
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
|
-
${
|
|
544
|
+
${classesWithApply.map((c) => ` ${c.name}: typeof ${c.name};`).join("\n")}
|
|
309
545
|
}): void {
|
|
310
|
-
${
|
|
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
|
|
384
|
-
async watch() {
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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.
|
|
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
|
|
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
|
|
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);
|