diff --git a/src/lib/utils/options/readers/typedoc.ts b/src/lib/utils/options/readers/typedoc.ts index ea8cc3226..44cb5f6af 100644 --- a/src/lib/utils/options/readers/typedoc.ts +++ b/src/lib/utils/options/readers/typedoc.ts @@ -25,7 +25,7 @@ export class TypeDocReader implements OptionsReader { */ read(container: Options, logger: Logger): void { const path = container.getValue('options'); - const file = this.findTypedocFile(path, logger); + const file = this.findTypedocFile(path); if (!file) { if (!container.isDefault('options')) { @@ -34,15 +34,43 @@ export class TypeDocReader implements OptionsReader { return; } - let data: any = require(file); - if (typeof data !== 'object') { + const seen = new Set(); + this.readFile(file, container, logger, seen); + } + + /** + * Read the given options file + any extended files. + * @param file + * @param container + * @param logger + */ + private readFile(file: string, container: Options, logger: Logger, seen: Set) { + if (seen.has(file)) { + logger.error(`Tried to load the options file ${file} multiple times.`); + return; + } + seen.add(file); + + const data: unknown = require(file); + + if (typeof data !== 'object' || !data) { logger.error(`The file ${file} is not an object.`); return; } + + if ('extends' in data) { + const extended: string[] = getStringArray(data['extends']); + for (const extendedFile of extended) { + // Extends is relative to the file it appears in. + this.readFile(Path.resolve(Path.dirname(file), extendedFile), container, logger, seen); + } + delete data['extends']; + } + // deprecate: data.src is alias to inputFiles as of 0.16, warn in 0.17, remove in 0.19 if ('src' in data && !('inputFiles' in data)) { - data.inputFiles = Array.isArray(data.src) ? data.src : [data.src]; - delete data.src; + data['inputFiles'] = getStringArray(data['src']); + delete data['src']; } container.setValues(data).match({ @@ -63,7 +91,7 @@ export class TypeDocReader implements OptionsReader { * @param logger * @return the typedoc.(js|json) file path or undefined */ - private findTypedocFile(path: string, logger: Logger): string | undefined { + private findTypedocFile(path: string): string | undefined { path = Path.resolve(path); return [ @@ -73,3 +101,7 @@ export class TypeDocReader implements OptionsReader { ].find(path => FS.existsSync(path) && FS.statSync(path).isFile()); } } + +function getStringArray(arg: unknown): string[] { + return Array.isArray(arg) ? arg.map(String) : [String(arg)]; +} diff --git a/src/test/utils/options/readers/data/circular-extends.json b/src/test/utils/options/readers/data/circular-extends.json new file mode 100644 index 000000000..c58ddc2d2 --- /dev/null +++ b/src/test/utils/options/readers/data/circular-extends.json @@ -0,0 +1,3 @@ +{ + "extends": ["./circular-extends.json"] +} diff --git a/src/test/utils/options/readers/data/extends.json b/src/test/utils/options/readers/data/extends.json new file mode 100644 index 000000000..a0cb9c9ce --- /dev/null +++ b/src/test/utils/options/readers/data/extends.json @@ -0,0 +1,4 @@ +{ + "extends": "./src.json", + "name": "extends" +} diff --git a/src/test/utils/options/readers/data/src.json b/src/test/utils/options/readers/data/src.json index 554b48a2f..676476b9a 100644 --- a/src/test/utils/options/readers/data/src.json +++ b/src/test/utils/options/readers/data/src.json @@ -1,3 +1,4 @@ { - "src": ["a"] + "src": ["a"], + "name": "src" } diff --git a/src/test/utils/options/readers/typedoc.test.ts b/src/test/utils/options/readers/typedoc.test.ts index 44db5c4ac..0d363b405 100644 --- a/src/test/utils/options/readers/typedoc.test.ts +++ b/src/test/utils/options/readers/typedoc.test.ts @@ -26,6 +26,11 @@ describe('Options - TypeDocReader', () => { equal(options.getValue('inputFiles'), ['a']); }); + test('Supports extends', join(__dirname, 'data/extends.json'), () => { + equal(options.getValue('name'), 'extends'); + equal(options.getValue('inputFiles'), ['a']); + }); + function testError(name: string, file: string) { it(name, () => { options.reset(); @@ -39,6 +44,7 @@ describe('Options - TypeDocReader', () => { testError('Errors if the file cannot be found', join(__dirname, 'data/non-existent-file.json')); testError('Errors if the data is invalid', join(__dirname, 'data/invalid.json')); testError('Errors if any set option errors', join(__dirname, 'data/unknown.json')); + testError('Errors if extends results in a loop', join(__dirname, 'data/circular-extends.json')); it('Does not error if the option file cannot be found but was not set.', () => { const options = new class LyingOptions extends Options {