Skip to content

Commit f4345a6

Browse files
authored
Merge pull request #2040 from citkane/issue#2024
New BOOTSTRAP_END event and ParameterType.Object
2 parents 9443d98 + a3acebd commit f4345a6

File tree

7 files changed

+114
-8
lines changed

7 files changed

+114
-8
lines changed

src/lib/application-events.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const ApplicationEvents = {
2+
BOOTSTRAP_END: "bootstrapEnd",
3+
};

src/lib/application.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { getLoadedPaths, hasBeenLoadedMultipleTimes } from "./utils/general";
3535
import { validateExports } from "./validation/exports";
3636
import { validateDocumentation } from "./validation/documentation";
3737
import { validateLinks } from "./validation/links";
38+
import { ApplicationEvents } from "./application-events";
3839

3940
// eslint-disable-next-line @typescript-eslint/no-var-requires
4041
const packageInfo = require("../../package.json") as {
@@ -161,6 +162,7 @@ export class Application extends ChildableComponent<
161162
)}`
162163
);
163164
}
165+
this.trigger(ApplicationEvents.BOOTSTRAP_END, this, options);
164166
}
165167

166168
/**

src/lib/utils/options/declaration.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,11 @@ export type KeyToDeclaration<K extends keyof TypeDocOptionMap> =
188188
: TypeDocOptionMap[K] extends string[]
189189
? ArrayDeclarationOption
190190
: unknown extends TypeDocOptionMap[K]
191-
? MixedDeclarationOption
191+
? MixedDeclarationOption | ObjectDeclarationOption
192192
: TypeDocOptionMap[K] extends ManuallyValidatedOption<unknown>
193-
? MixedDeclarationOption & { validate(value: unknown): void }
193+
?
194+
| (MixedDeclarationOption & { validate(value: unknown): void })
195+
| (ObjectDeclarationOption & { validate(value: unknown): void })
194196
: TypeDocOptionMap[K] extends Record<string, boolean>
195197
? FlagsDeclarationOption<TypeDocOptionMap[K]>
196198
: TypeDocOptionMap[K] extends Record<string | number, infer U>
@@ -225,6 +227,10 @@ export enum ParameterType {
225227
* Resolved according to the config directory unless it starts with `**`, after skipping any leading `!` and `#` characters.
226228
*/
227229
GlobArray,
230+
/**
231+
* An unopinionated object that preserves default settings unless explicitly overridden
232+
*/
233+
Object,
228234
/**
229235
* An object with true/false flags
230236
*/
@@ -341,6 +347,20 @@ export interface MixedDeclarationOption extends DeclarationOptionBase {
341347
validate?: (value: unknown) => void;
342348
}
343349

350+
export interface ObjectDeclarationOption extends DeclarationOptionBase {
351+
type: ParameterType.Object;
352+
353+
/**
354+
* If not specified defaults to undefined.
355+
*/
356+
defaultValue?: unknown;
357+
358+
/**
359+
* An optional validation function that validates a potential value of this option.
360+
* The function must throw an Error if the validation fails and should do nothing otherwise.
361+
*/
362+
validate?: (value: unknown) => void;
363+
}
344364
export interface MapDeclarationOption<T> extends DeclarationOptionBase {
345365
type: ParameterType.Map;
346366

@@ -378,6 +398,7 @@ export type DeclarationOption =
378398
| NumberDeclarationOption
379399
| BooleanDeclarationOption
380400
| MixedDeclarationOption
401+
| ObjectDeclarationOption
381402
| MapDeclarationOption<unknown>
382403
| ArrayDeclarationOption
383404
| FlagsDeclarationOption<Record<string, boolean>>;
@@ -388,6 +409,7 @@ export interface ParameterTypeToOptionTypeMap {
388409
[ParameterType.Number]: number;
389410
[ParameterType.Boolean]: boolean;
390411
[ParameterType.Mixed]: unknown;
412+
[ParameterType.Object]: unknown;
391413
[ParameterType.Array]: string[];
392414
[ParameterType.PathArray]: string[];
393415
[ParameterType.ModuleArray]: string[];
@@ -409,7 +431,8 @@ const converters: {
409431
[K in ParameterType]: (
410432
value: unknown,
411433
option: DeclarationOption & { type: K },
412-
configPath: string
434+
configPath: string,
435+
oldValue: unknown
413436
) => ParameterTypeToOptionTypeMap[K];
414437
} = {
415438
[ParameterType.String](value, option) {
@@ -503,6 +526,12 @@ const converters: {
503526
option.validate?.(value);
504527
return value;
505528
},
529+
[ParameterType.Object](value, option, _configPath, oldValue) {
530+
option.validate?.(value);
531+
if (typeof oldValue !== "undefined")
532+
value = { ...(oldValue as {}), ...(value as {}) };
533+
return value;
534+
},
506535
[ParameterType.Flags](value, option) {
507536
if (typeof value === "boolean") {
508537
value = Object.fromEntries(
@@ -554,16 +583,18 @@ const converters: {
554583
export function convert(
555584
value: unknown,
556585
option: DeclarationOption,
557-
configPath: string
586+
configPath: string,
587+
oldValue?: unknown
558588
): unknown {
559589
const _converters = converters as Record<
560590
ParameterType,
561-
(v: unknown, o: DeclarationOption, c: string) => unknown
591+
(v: unknown, o: DeclarationOption, c: string, ov: unknown) => unknown
562592
>;
563593
return _converters[option.type ?? ParameterType.String](
564594
value,
565595
option,
566-
configPath
596+
configPath,
597+
oldValue
567598
);
568599
}
569600

@@ -596,6 +627,9 @@ const defaultGetters: {
596627
[ParameterType.Mixed](option) {
597628
return option.defaultValue;
598629
},
630+
[ParameterType.Object](option) {
631+
return option.defaultValue;
632+
},
599633
[ParameterType.Array](option) {
600634
return option.defaultValue?.slice() ?? [];
601635
},

src/lib/utils/options/options.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,15 @@ export class Options {
301301
);
302302
}
303303

304+
let oldValue = this._values[declaration.name];
305+
if (typeof oldValue === "undefined")
306+
oldValue = getDefaultValue(declaration);
307+
304308
const converted = convert(
305309
value,
306310
declaration,
307-
configPath ?? process.cwd()
311+
configPath ?? process.cwd(),
312+
oldValue
308313
);
309314

310315
if (declaration.type === ParameterType.Flags) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
() => null;

src/test/issueTests.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2+
deepStrictEqual,
23
deepStrictEqual as equal,
34
notDeepStrictEqual as notEqual,
45
ok,
56
} from "assert";
6-
import type { Application } from "../lib/application";
7+
import { Application } from "../lib/application";
8+
import { ApplicationEvents } from "../lib/application-events";
79
import {
810
DeclarationReflection,
911
ProjectReflection,
@@ -18,6 +20,7 @@ import {
1820
IntrinsicType,
1921
} from "../lib/models";
2022
import type { InlineTagDisplayPart } from "../lib/models/comments/comment";
23+
import { ParameterType, TypeDocOptionMap } from "../lib/utils";
2124
import { getConverter2App } from "./programs";
2225
import type { TestLogger } from "./TestLogger";
2326

@@ -725,6 +728,40 @@ export const issueTests: {
725728
);
726729
},
727730

731+
gh2024() {
732+
const name = "objectOptions" as keyof TypeDocOptionMap;
733+
const defaultValue = {
734+
neverChange: "ok",
735+
foo: "foo",
736+
};
737+
const app = new Application();
738+
app.options.addDeclaration({
739+
help: "Test option parsing with default values from plugins",
740+
name,
741+
type: ParameterType.Object,
742+
defaultValue,
743+
});
744+
deepStrictEqual(app.options.getValue(name), defaultValue);
745+
app.options.setValue(name, { foo: "bar" });
746+
const newVal = app.options.getValue(name) as typeof defaultValue;
747+
equal(newVal.foo, "bar");
748+
equal(newVal.neverChange, "ok");
749+
app.options.setValue("out", "testOutString");
750+
app.on(ApplicationEvents.BOOTSTRAP_END, () => {
751+
equal(app.options.getValue("out"), "");
752+
app.options.setValue("out", "testOutString");
753+
app.options.setValue(name, { foo: "foo" });
754+
});
755+
app.bootstrap();
756+
app.options.setValue(name, {});
757+
app.convert();
758+
deepStrictEqual(app.options.getValue(name), defaultValue);
759+
equal(
760+
app.options.getValue("out").trim().endsWith("testOutString"),
761+
true
762+
);
763+
},
764+
728765
gh2031(project, logger) {
729766
const sig = query(project, "MyClass.aMethod").signatures![0];
730767
const summaryLink = sig.comment?.summary[0];

src/test/utils/options/declaration.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getDefaultValue,
88
MapDeclarationOption,
99
MixedDeclarationOption,
10+
ObjectDeclarationOption,
1011
NumberDeclarationOption,
1112
ParameterType,
1213
StringDeclarationOption,
@@ -321,6 +322,29 @@ describe("Options - conversions", () => {
321322
new Error("test must not be a number")
322323
);
323324
});
325+
it("Passes through object", () => {
326+
const data = {};
327+
equal(convert(data, optionWithType(ParameterType.Object), ""), data);
328+
});
329+
330+
it("Validates object options", () => {
331+
const declaration: ObjectDeclarationOption = {
332+
name: "test",
333+
help: "",
334+
type: ParameterType.Object,
335+
defaultValue: "default",
336+
validate: (value: unknown) => {
337+
if (typeof value !== "object" || Array.isArray(value)) {
338+
throw new Error("test must be an object");
339+
}
340+
},
341+
};
342+
equal(convert({}, declaration, ""), {});
343+
throws(
344+
() => convert(1, declaration, ""),
345+
new Error("test must be an object")
346+
);
347+
});
324348
});
325349

326350
describe("Options - default values", () => {

0 commit comments

Comments
 (0)