diff --git a/src/lib/converter/plugins/PackagePlugin.ts b/src/lib/converter/plugins/PackagePlugin.ts index 27daef1d4..96901b44a 100644 --- a/src/lib/converter/plugins/PackagePlugin.ts +++ b/src/lib/converter/plugins/PackagePlugin.ts @@ -6,6 +6,7 @@ import { Converter } from "../converter"; import { Context } from "../context"; import { BindOption, readFile } from "../../utils"; import { getCommonDirectory } from "../../utils/fs"; +import { join } from "path"; /** * A handler that tries to find the package.json and readme.md files of the @@ -46,12 +47,11 @@ export class PackagePlugin extends ConverterComponent { this.readmeFile = undefined; this.packageFile = undefined; - let readme = this.readme; - const noReadmeFile = readme === "none"; - if (!noReadmeFile && readme) { - readme = Path.resolve(readme); - if (FS.existsSync(readme)) { - this.readmeFile = readme; + // Path will be resolved already. This is kind of ugly, but... + const noReadmeFile = this.readme == join(process.cwd(), "none"); + if (!noReadmeFile && this.readme) { + if (FS.existsSync(this.readme)) { + this.readmeFile = this.readme; } } @@ -61,11 +61,7 @@ export class PackagePlugin extends ConverterComponent { dirName === Path.resolve(Path.join(dirName, "..")); let dirName = Path.resolve( - getCommonDirectory( - this.application.options - .getValue("entryPoints") - .map((path) => Path.resolve(path)) - ) + getCommonDirectory(this.application.options.getValue("entryPoints")) ); this.application.logger.verbose(`Begin readme search at ${dirName}`); while (!packageAndReadmeFound() && !reachedTopDirectory(dirName)) { diff --git a/src/lib/output/plugins/MarkedPlugin.ts b/src/lib/output/plugins/MarkedPlugin.ts index 52d8454ca..7e379156b 100644 --- a/src/lib/output/plugins/MarkedPlugin.ts +++ b/src/lib/output/plugins/MarkedPlugin.ts @@ -200,28 +200,31 @@ output file : delete this.includes; if (this.includeSource) { - const includes = Path.resolve(this.includeSource); if ( - fs.existsSync(includes) && - fs.statSync(includes).isDirectory() + fs.existsSync(this.includeSource) && + fs.statSync(this.includeSource).isDirectory() ) { - this.includes = includes; + this.includes = this.includeSource; } else { this.application.logger.warn( - "Could not find provided includes directory: " + includes + "Could not find provided includes directory: " + + this.includeSource ); } } if (this.mediaSource) { - const media = Path.resolve(this.mediaSource); - if (fs.existsSync(media) && fs.statSync(media).isDirectory()) { + if ( + fs.existsSync(this.mediaSource) && + fs.statSync(this.mediaSource).isDirectory() + ) { this.mediaDirectory = Path.join(event.outputDirectory, "media"); - copySync(media, this.mediaDirectory); + copySync(this.mediaSource, this.mediaDirectory); } else { this.mediaDirectory = undefined; this.application.logger.warn( - "Could not find provided media directory: " + media + "Could not find provided media directory: " + + this.mediaSource ); } } diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index 091c53ae0..eddf180ae 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -1,6 +1,7 @@ import { Theme as ShikiTheme } from "shiki"; import { LogLevel } from "../loggers"; import { SortStrategy } from "../sort"; +import { isAbsolute, join, resolve } from "path"; /** * An interface describing all TypeDoc specific options. Generated from a @@ -114,11 +115,27 @@ export enum ParameterHint { export enum ParameterType { String, + /** + * Resolved according to the config directory. + */ + Path, Number, Boolean, Map, Mixed, Array, + /** + * Resolved according to the config directory. + */ + PathArray, + /** + * Resolved according to the config directory if it starts with `.` + */ + ModuleArray, + /** + * Resolved according to the config directory unless it starts with `**`, after skipping any leading `!` and `#` characters. + */ + GlobArray, } export interface DeclarationOptionBase { @@ -140,10 +157,14 @@ export interface DeclarationOptionBase { } export interface StringDeclarationOption extends DeclarationOptionBase { - type?: ParameterType.String; + /** + * Specifies the resolution strategy. If `Path` is provided, values will be resolved according to their + * location in a file. If `String` or no value is provided, values will not be resolved. + */ + type?: ParameterType.String | ParameterType.Path; /** - * If not specified defaults to the empty string. + * If not specified defaults to the empty string for both `String` and `Path`. */ defaultValue?: string; @@ -194,7 +215,11 @@ export interface BooleanDeclarationOption extends DeclarationOptionBase { } export interface ArrayDeclarationOption extends DeclarationOptionBase { - type: ParameterType.Array; + type: + | ParameterType.Array + | ParameterType.PathArray + | ParameterType.ModuleArray + | ParameterType.GlobArray; /** * If not specified defaults to an empty array. @@ -253,20 +278,122 @@ export type DeclarationOption = | MapDeclarationOption | ArrayDeclarationOption; +interface ParameterTypeToOptionTypeMap { + [ParameterType.String]: string; + [ParameterType.Path]: string; + [ParameterType.Number]: number; + [ParameterType.Boolean]: boolean; + [ParameterType.Mixed]: unknown; + [ParameterType.Array]: string[]; + [ParameterType.PathArray]: string[]; + [ParameterType.ModuleArray]: string[]; + [ParameterType.GlobArray]: string[]; + + // Special.. avoid this if possible. + [ParameterType.Map]: unknown; +} + export type DeclarationOptionToOptionType = - T extends StringDeclarationOption - ? string - : T extends NumberDeclarationOption - ? number - : T extends BooleanDeclarationOption - ? boolean - : T extends MixedDeclarationOption - ? unknown - : T extends MapDeclarationOption + T extends MapDeclarationOption ? U - : T extends ArrayDeclarationOption - ? string[] - : never; + : ParameterTypeToOptionTypeMap[Exclude]; + +const converters: { + [K in ParameterType]: ( + value: unknown, + option: DeclarationOption & { type: K }, + configPath: string + ) => ParameterTypeToOptionTypeMap[K]; +} = { + [ParameterType.String](value, option) { + const stringValue = value == null ? "" : String(value); + option.validate?.(stringValue); + return stringValue; + }, + [ParameterType.Path](value, option, configPath) { + const stringValue = + value == null ? "" : resolve(configPath, String(value)); + option.validate?.(stringValue); + return stringValue; + }, + [ParameterType.Number](value, option) { + const numValue = parseInt(String(value), 10) || 0; + if (!valueIsWithinBounds(numValue, option.minValue, option.maxValue)) { + throw new Error( + getBoundsError(option.name, option.minValue, option.maxValue) + ); + } + option.validate?.(numValue); + return numValue; + }, + [ParameterType.Boolean](value) { + return !!value; + }, + [ParameterType.Array](value, option) { + let strArrValue = new Array(); + if (Array.isArray(value)) { + strArrValue = value.map(String); + } else if (typeof value === "string") { + strArrValue = [value]; + } + option.validate?.(strArrValue); + return strArrValue; + }, + [ParameterType.PathArray](value, option, configPath) { + let strArrValue = new Array(); + if (Array.isArray(value)) { + strArrValue = value.map(String); + } else if (typeof value === "string") { + strArrValue = [value]; + } + strArrValue = strArrValue.map((path) => resolve(configPath, path)); + option.validate?.(strArrValue); + return strArrValue; + }, + [ParameterType.ModuleArray](value, option, configPath) { + let strArrValue = new Array(); + if (Array.isArray(value)) { + strArrValue = value.map(String); + } else if (typeof value === "string") { + strArrValue = [value]; + } + strArrValue = resolveModulePaths(strArrValue, configPath); + option.validate?.(strArrValue); + return strArrValue; + }, + [ParameterType.GlobArray](value, option, configPath) { + let strArrValue = new Array(); + if (Array.isArray(value)) { + strArrValue = value.map(String); + } else if (typeof value === "string") { + strArrValue = [value]; + } + strArrValue = resolveGlobPaths(strArrValue, configPath); + option.validate?.(strArrValue); + return strArrValue; + }, + [ParameterType.Map](value, option) { + const key = String(value).toLowerCase(); + if (option.map instanceof Map) { + if (option.map.has(key)) { + return option.map.get(key); + } else if ([...option.map.values()].includes(value)) { + return value; + } + } else if (key in option.map) { + return option.map[key]; + } else if (Object.values(option.map).includes(value)) { + return value; + } + throw new Error( + option.mapError ?? getMapError(option.map, option.name) + ); + }, + [ParameterType.Mixed](value, option) { + option.validate?.(value); + return value; + }, +}; /** * The default conversion function used by the Options container. Readers may @@ -276,78 +403,99 @@ export type DeclarationOptionToOptionType = * @param option The option for which the value should be converted. * @returns The result of the conversion. Might be the value or an error. */ -export function convert( +export function convert( value: unknown, - option: T -): DeclarationOptionToOptionType; -export function convert(value: unknown, option: MapDeclarationOption): T; -export function convert(value: unknown, option: DeclarationOption): unknown { - switch (option.type) { - case undefined: - case ParameterType.String: { - const stringValue = value == null ? "" : String(value); - if (option.validate) { - option.validate(stringValue); - } - return stringValue; - } - case ParameterType.Number: { - const numValue = parseInt(String(value), 10) || 0; - if ( - !valueIsWithinBounds(numValue, option.minValue, option.maxValue) - ) { - throw new Error( - getBoundsError( - option.name, - option.minValue, - option.maxValue - ) - ); - } - if (option.validate) { - option.validate(numValue); - } - return numValue; + option: DeclarationOption, + configPath: string +): unknown { + const _converters = converters as Record< + ParameterType, + (v: unknown, o: DeclarationOption, c: string) => unknown + >; + return _converters[option.type ?? ParameterType.String]( + value, + option, + configPath + ); +} + +const defaultGetters: { + [K in ParameterType]: ( + option: DeclarationOption & { type: K } + ) => ParameterTypeToOptionTypeMap[K]; +} = { + [ParameterType.String](option) { + return option.defaultValue ?? ""; + }, + [ParameterType.Path](option) { + const defaultStr = option.defaultValue ?? ""; + if (defaultStr == "") { + return ""; } + return isAbsolute(defaultStr) + ? defaultStr + : join(process.cwd(), defaultStr); + }, + [ParameterType.Number](option) { + return option.defaultValue ?? 0; + }, + [ParameterType.Boolean](option) { + return option.defaultValue ?? false; + }, + [ParameterType.Map](option) { + return option.defaultValue; + }, + [ParameterType.Mixed](option) { + return option.defaultValue; + }, + [ParameterType.Array](option) { + return option.defaultValue ?? []; + }, + [ParameterType.PathArray](option) { + return ( + option.defaultValue?.map((value) => + resolve(process.cwd(), value) + ) ?? [] + ); + }, + [ParameterType.ModuleArray](option) { + return ( + option.defaultValue?.map((value) => + value.startsWith(".") ? resolve(process.cwd(), value) : value + ) ?? [] + ); + }, + [ParameterType.GlobArray](option) { + return resolveGlobPaths(option.defaultValue ?? [], process.cwd()); + }, +}; - case ParameterType.Boolean: - return Boolean(value); +export function getDefaultValue(option: DeclarationOption) { + const getters = defaultGetters as Record< + ParameterType, + (o: DeclarationOption) => unknown + >; + return getters[option.type ?? ParameterType.String](option); +} - case ParameterType.Array: { - let strArrValue = new Array(); - if (Array.isArray(value)) { - strArrValue = value.map(String); - } else if (typeof value === "string") { - strArrValue = value.split(","); - } - if (option.validate) { - option.validate(strArrValue); - } - return strArrValue; +function resolveGlobPaths(globs: readonly string[], configPath: string) { + return globs.map((path) => { + const start = path.match(/^[!#]+/)?.[0] ?? ""; + const remaining = path.substr(start.length); + if (!remaining.startsWith("**")) { + return start + resolve(configPath, remaining); } - case ParameterType.Map: { - const key = String(value).toLowerCase(); - if (option.map instanceof Map) { - if (option.map.has(key)) { - return option.map.get(key); - } else if ([...option.map.values()].includes(value)) { - return value; - } - } else if (key in option.map) { - return option.map[key]; - } else if (Object.values(option.map).includes(value)) { - return value; - } - throw new Error( - option.mapError ?? getMapError(option.map, option.name) - ); + return start + remaining; + }); +} + +function resolveModulePaths(modules: readonly string[], configPath: string) { + return modules.map((path) => { + if (path.startsWith(".")) { + return resolve(configPath, path); } - case ParameterType.Mixed: - if (option.validate) { - option.validate(value); - } - return value; - } + return path; + }); } /** @@ -393,10 +541,9 @@ function getBoundsError( return `${name} must be between ${minValue} and ${maxValue}`; } else if (isFiniteNumber(minValue)) { return `${name} must be >= ${minValue}`; - } else if (isFiniteNumber(maxValue)) { + } else { return `${name} must be <= ${maxValue}`; } - throw new Error("Unreachable"); } /** diff --git a/src/lib/utils/options/options.ts b/src/lib/utils/options/options.ts index 654f5425e..7febcaa32 100644 --- a/src/lib/utils/options/options.ts +++ b/src/lib/utils/options/options.ts @@ -1,20 +1,19 @@ import { cloneDeep } from "lodash"; import * as ts from "typescript"; - +import { NeverIfInternal } from ".."; +import { Application } from "../../.."; +import { insertPrioritySorted, unique } from "../array"; +import { Logger } from "../loggers"; import { convert, DeclarationOption, + getDefaultValue, KeyToDeclaration, - ParameterType, TypeDocOptionMap, TypeDocOptions, TypeDocOptionValues, } from "./declaration"; -import { Logger } from "../loggers"; -import { insertPrioritySorted, unique } from "../array"; import { addTypeDocOptions } from "./sources"; -import { Application } from "../../.."; -import { NeverIfInternal } from ".."; /** * Describes an option reader that discovers user configuration and converts it to the @@ -107,7 +106,7 @@ export class Options { */ reset() { for (const declaration of this.getDeclarations()) { - this.setOptionValueToDefault(declaration); + this._values[declaration.name] = getDefaultValue(declaration); } this._setOptions.clear(); this._compilerOptions = {}; @@ -163,7 +162,7 @@ export class Options { this._declarations.set(declaration.name, declaration); } - this.setOptionValueToDefault(declaration); + this._values[declaration.name] = getDefaultValue(declaration); } /** @@ -249,16 +248,19 @@ export class Options { * Sets the given declared option. Throws if setting the option fails. * @param name * @param value + * @param configPath the directory to resolve Path type values against */ setValue( name: K, - value: TypeDocOptions[K] + value: TypeDocOptions[K], + configPath?: string ): void; setValue( name: NeverIfInternal, - value: NeverIfInternal + value: NeverIfInternal, + configPath?: NeverIfInternal ): void; - setValue(name: string, value: unknown): void { + setValue(name: string, value: unknown, configPath?: string): void { const declaration = this.getDeclaration(name); if (!declaration) { throw new Error( @@ -266,7 +268,11 @@ export class Options { ); } - const converted = convert(value, declaration); + const converted = convert( + value, + declaration, + configPath ?? process.cwd() + ); this._values[declaration.name] = converted; this._setOptions.add(name); } @@ -312,32 +318,6 @@ export class Options { this._compilerOptions = cloneDeep(options); this._projectReferences = projectReferences ?? []; } - - /** - * Sets the value of a given option to its default value. - * @param declaration The option whose value should be reset. - */ - private setOptionValueToDefault( - declaration: Readonly - ): void { - this._values[declaration.name] = - this.getDefaultOptionValue(declaration); - } - - private getDefaultOptionValue( - declaration: Readonly - ): unknown { - // No need to convert the defaultValue for a map type as it has to be of a specific type - // Also don't use convert for number options to allow every possible number as a default value. - if ( - declaration.type === ParameterType.Map || - declaration.type === ParameterType.Number - ) { - return declaration.defaultValue; - } else { - return convert(declaration.defaultValue, declaration); - } - } } /** diff --git a/src/lib/utils/options/readers/arguments.ts b/src/lib/utils/options/readers/arguments.ts index 75845f5a8..3e21fa49c 100644 --- a/src/lib/utils/options/readers/arguments.ts +++ b/src/lib/utils/options/readers/arguments.ts @@ -2,6 +2,13 @@ import { OptionsReader, Options } from ".."; import { Logger } from "../../loggers"; import { ParameterType } from "../declaration"; +const ARRAY_OPTION_TYPES = new Set([ + ParameterType.Array, + ParameterType.PathArray, + ParameterType.ModuleArray, + ParameterType.GlobArray, +]); + /** * Obtains option values from command-line arguments */ @@ -39,7 +46,7 @@ export class ArgumentsReader implements OptionsReader { : options.getDeclaration("entryPoints"); if (decl) { - if (seen.has(decl.name) && decl.type === ParameterType.Array) { + if (seen.has(decl.name) && ARRAY_OPTION_TYPES.has(decl.type)) { trySet( decl.name, (options.getValue(decl.name) as string[]).concat( diff --git a/src/lib/utils/options/readers/tsconfig.ts b/src/lib/utils/options/readers/tsconfig.ts index e6076ace8..0cb1d9ca4 100644 --- a/src/lib/utils/options/readers/tsconfig.ts +++ b/src/lib/utils/options/readers/tsconfig.ts @@ -1,4 +1,4 @@ -import { resolve, basename } from "path"; +import { resolve, basename, join } from "path"; import { existsSync, statSync } from "fs"; import * as ts from "typescript"; @@ -87,7 +87,11 @@ export class TSConfigReader implements OptionsReader { for (const [key, val] of Object.entries(typedocOptions || {})) { try { // We catch the error, so can ignore the strict type checks - container.setValue(key as never, val as never); + container.setValue( + key as never, + val as never, + join(fileToRead, "..") + ); } catch (error) { logger.error(error.message); } diff --git a/src/lib/utils/options/readers/typedoc.ts b/src/lib/utils/options/readers/typedoc.ts index f67cbdca7..21e4ae828 100644 --- a/src/lib/utils/options/readers/typedoc.ts +++ b/src/lib/utils/options/readers/typedoc.ts @@ -1,4 +1,4 @@ -import * as Path from "path"; +import { join, dirname, resolve } from "path"; import * as FS from "fs"; import { cloneDeep } from "lodash"; @@ -77,7 +77,7 @@ export class TypeDocReader implements OptionsReader { for (const extendedFile of extended) { // Extends is relative to the file it appears in. this.readFile( - Path.resolve(Path.dirname(file), extendedFile), + resolve(dirname(file), extendedFile), container, logger, seen @@ -88,15 +88,11 @@ export class TypeDocReader implements OptionsReader { for (const [key, val] of Object.entries(data)) { try { - // The "packages" option is an array of paths and should be interpreted as relative to the typedoc.json - if (key === "packages" && Array.isArray(val)) { - container.setValue( - key, - val.map((e) => Path.resolve(file, "..", e)) - ); - } else { - container.setValue(key, val); - } + container.setValue( + key as never, + val as never, + resolve(dirname(file)) + ); } catch (error) { logger.error(error.message); } @@ -112,12 +108,12 @@ export class TypeDocReader implements OptionsReader { * @return the typedoc.(js|json) file path or undefined */ private findTypedocFile(path: string): string | undefined { - path = Path.resolve(path); + path = resolve(path); return [ path, - Path.join(path, "typedoc.json"), - Path.join(path, "typedoc.js"), + join(path, "typedoc.json"), + join(path, "typedoc.js"), ].find((path) => FS.existsSync(path) && FS.statSync(path).isFile()); } } diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index f7aad3014..f946058c3 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -6,12 +6,14 @@ import { SORT_STRATEGIES } from "../../sort"; export function addTypeDocOptions(options: Pick) { options.addDeclaration({ + type: ParameterType.Path, name: "options", help: "Specify a json option file that should be loaded. If not specified TypeDoc will look for 'typedoc.json' in the current directory", hint: ParameterHint.File, defaultValue: process.cwd(), }); options.addDeclaration({ + type: ParameterType.Path, name: "tsconfig", help: "Specify a TypeScript config file that should be loaded. If not specified TypeDoc will look for 'tsconfig.json' in the current directory.", hint: ParameterHint.File, @@ -22,25 +24,25 @@ export function addTypeDocOptions(options: Pick) { help: "Specify one or more package folders from which a package.json file should be loaded to determine the entry points. Your JS files must have sourcemaps for this to work." + "If the root of an npm or Yarn workspace is given, the packages specified in `workspaces` will be loaded.", - type: ParameterType.Array, + type: ParameterType.PathArray, defaultValue: [], }); options.addDeclaration({ name: "entryPoints", help: "The entry points of your library, which files should be documented as available to consumers.", - type: ParameterType.Array, + type: ParameterType.PathArray, }); options.addDeclaration({ name: "exclude", - help: "Define paths to be excluded when expanding a directory that was specified as an entry point.", - type: ParameterType.Array, + help: "Define patterns to be excluded when expanding a directory that was specified as an entry point.", + type: ParameterType.GlobArray, }); options.addDeclaration({ name: "externalPattern", help: "Define patterns for files that should be considered being external.", - type: ParameterType.Array, + type: ParameterType.GlobArray, defaultValue: ["**/node_modules/**"], }); options.addDeclaration({ @@ -76,11 +78,13 @@ export function addTypeDocOptions(options: Pick) { options.addDeclaration({ name: "includes", help: "Specifies the location to look for included documents (use [[include:FILENAME]] in comments).", + type: ParameterType.Path, hint: ParameterHint.Directory, }); options.addDeclaration({ name: "media", help: "Specifies the location with media files that should be copied to the output directory.", + type: ParameterType.Path, hint: ParameterHint.Directory, }); @@ -103,11 +107,13 @@ export function addTypeDocOptions(options: Pick) { options.addDeclaration({ name: "out", help: "Specifies the location the documentation should be written to.", + type: ParameterType.Path, hint: ParameterHint.Directory, }); options.addDeclaration({ name: "json", help: "Specifies the location and filename a JSON file describing the project is written to.", + type: ParameterType.Path, hint: ParameterHint.File, }); options.addDeclaration({ @@ -119,7 +125,9 @@ export function addTypeDocOptions(options: Pick) { options.addDeclaration({ name: "theme", - help: "Specify the path to the theme that should be used, or 'default' or 'minimal' to use built-in themes.", + help: + "Specify the path to the theme that should be used, or 'default' or 'minimal' to use built-in themes." + + "Note: Not resolved according to the config file location, always resolved according to cwd.", type: ParameterType.String, defaultValue: "default", }); @@ -156,6 +164,7 @@ export function addTypeDocOptions(options: Pick) { options.addDeclaration({ name: "readme", help: "Path to the readme file that should be displayed on the index page. Pass `none` to disable the index page and start the documentation on the globals page.", + type: ParameterType.Path, }); options.addDeclaration({ name: "defaultCategory", @@ -249,7 +258,7 @@ export function addTypeDocOptions(options: Pick) { options.addDeclaration({ name: "plugin", help: "Specify the npm plugins that should be loaded. Omit to load all installed plugins, set to 'none' to load no plugins.", - type: ParameterType.Array, + type: ParameterType.ModuleArray, }); options.addDeclaration({ name: "logger", diff --git a/src/lib/utils/paths.ts b/src/lib/utils/paths.ts index 5bd5c1e87..382355264 100644 --- a/src/lib/utils/paths.ts +++ b/src/lib/utils/paths.ts @@ -1,30 +1,5 @@ -import * as Path from "path"; import { Minimatch, IMinimatch } from "minimatch"; - -const unix = Path.sep === "/"; - -function normalize(pattern: string): string { - if (pattern.startsWith("!") || pattern.startsWith("#")) { - return pattern[0] + normalize(pattern.substr(1)); - } - - if (unix) { - pattern = pattern.replace(/[\\]/g, "/").replace(/^\w:/, ""); - } - - // pattern paths not starting with '**' are resolved even if it is an - // absolute path, to ensure correct format for the current OS - if (pattern.substr(0, 2) !== "**") { - pattern = Path.resolve(pattern); - } - - // On Windows we transform `\` to `/` to unify the way paths are intepreted - if (!unix) { - pattern = pattern.replace(/[\\]/g, "/"); - } - - return pattern; -} +import { normalizePath } from "./fs"; /** * Convert array of glob patterns to array of minimatch instances. @@ -33,6 +8,9 @@ function normalize(pattern: string): string { */ export function createMinimatch(patterns: string[]): IMinimatch[] { return patterns.map( - (pattern) => new Minimatch(normalize(pattern), { dot: true }) + (pattern) => + new Minimatch(normalizePath(pattern).replace(/^\w:\//, ""), { + dot: true, + }) ); } diff --git a/src/lib/utils/plugins.ts b/src/lib/utils/plugins.ts index 186ce21d1..578574320 100644 --- a/src/lib/utils/plugins.ts +++ b/src/lib/utils/plugins.ts @@ -5,7 +5,11 @@ import type { Application } from "../application"; import type { Logger } from "./loggers"; export function loadPlugins(app: Application, plugins: readonly string[]) { - for (const plugin of resolvePluginPaths(plugins)) { + if (plugins.includes("none")) { + return; + } + + for (const plugin of plugins) { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const instance = require(plugin); @@ -118,14 +122,3 @@ function isPlugin(info: any): boolean { keyword.toLocaleLowerCase() === "typedocplugin" ); } - -function resolvePluginPaths(plugins: readonly string[]) { - const cwd = process.cwd(); - return plugins.map((plugin) => { - // treat plugins that start with `.` as relative, requiring resolution - if (plugin.startsWith(".")) { - return Path.resolve(cwd, plugin); - } - return plugin; - }); -} diff --git a/src/test/project.test.ts b/src/test/project.test.ts index 19f068a61..7a40447db 100644 --- a/src/test/project.test.ts +++ b/src/test/project.test.ts @@ -25,7 +25,7 @@ describe("Project", function () { Assert.strictEqual(result[1], "bar", "Wrong split"); }); - it("unmachted quotes", function () { + it("unmatched quotes", function () { result = splitUnquotedString('"foo.d', "."); Assert.strictEqual(result.length, 2, "Wrong length"); Assert.strictEqual(result[0], '"foo', "Wrong split"); diff --git a/src/test/utils/options/declaration.test.ts b/src/test/utils/options/declaration.test.ts index d150e6da4..71d653498 100644 --- a/src/test/utils/options/declaration.test.ts +++ b/src/test/utils/options/declaration.test.ts @@ -1,8 +1,10 @@ -import { deepStrictEqual as equal, throws } from "assert"; +import { deepStrictEqual as equal, ok, throws } from "assert"; +import { join, resolve } from "path"; import { ArrayDeclarationOption, convert, DeclarationOption, + getDefaultValue, MapDeclarationOption, MixedDeclarationOption, NumberDeclarationOption, @@ -10,19 +12,19 @@ import { StringDeclarationOption, } from "../../../lib/utils/options/declaration"; -describe("Options - Default convert function", () => { +describe("Options - conversions", () => { const optionWithType = (type: ParameterType) => ({ type, - defaultValue: null, + defaultValue: undefined, name: "test", help: "", } as DeclarationOption); it("Converts to numbers", () => { - equal(convert("123", optionWithType(ParameterType.Number)), 123); - equal(convert("a", optionWithType(ParameterType.Number)), 0); - equal(convert(NaN, optionWithType(ParameterType.Number)), 0); + equal(convert("123", optionWithType(ParameterType.Number), ""), 123); + equal(convert("a", optionWithType(ParameterType.Number), ""), 0); + equal(convert(NaN, optionWithType(ParameterType.Number), ""), 0); }); it("Converts to number if value is the lowest allowed value for a number option", () => { @@ -34,7 +36,7 @@ describe("Options - Default convert function", () => { maxValue: 10, defaultValue: 1, }; - equal(convert(1, declaration), 1); + equal(convert(1, declaration, ""), 1); }); it("Generates an error if value is too low for a number option", () => { @@ -47,11 +49,39 @@ describe("Options - Default convert function", () => { defaultValue: 1, }; throws( - () => convert(0, declaration), + () => convert(0, declaration, ""), new Error("test must be between 1 and 10") ); }); + it("Generates an error if value is too low for a number option with no max", () => { + const declaration: NumberDeclarationOption = { + name: "test", + help: "", + type: ParameterType.Number, + minValue: 1, + defaultValue: 1, + }; + throws( + () => convert(0, declaration, ""), + new Error("test must be >= 1") + ); + }); + + it("Generates an error if value is too high for a number option with no min", () => { + const declaration: NumberDeclarationOption = { + name: "test", + help: "", + type: ParameterType.Number, + maxValue: 10, + defaultValue: 1, + }; + throws( + () => convert(11, declaration, ""), + new Error("test must be <= 10") + ); + }); + it("Converts to number if value is the highest allowed value for a number option", () => { const declaration: NumberDeclarationOption = { name: "test", @@ -61,7 +91,7 @@ describe("Options - Default convert function", () => { maxValue: 10, defaultValue: 1, }; - equal(convert(10, declaration), 10); + equal(convert(10, declaration, ""), 10); }); it("Generates an error if value is too high for a number option", () => { @@ -74,7 +104,7 @@ describe("Options - Default convert function", () => { defaultValue: 1, }; throws( - () => convert(11, declaration), + () => convert(11, declaration, ""), new Error("test must be between 1 and 10") ); }); @@ -90,18 +120,24 @@ describe("Options - Default convert function", () => { } }, }; - equal(convert(0, declaration), 0); - equal(convert(2, declaration), 2); - equal(convert(4, declaration), 4); - throws(() => convert(1, declaration), new Error("test must be even")); + equal(convert(0, declaration, ""), 0); + equal(convert(2, declaration, ""), 2); + equal(convert(4, declaration, ""), 4); + throws( + () => convert(1, declaration, ""), + new Error("test must be even") + ); }); it("Converts to strings", () => { - equal(convert("123", optionWithType(ParameterType.String)), "123"); - equal(convert(123, optionWithType(ParameterType.String)), "123"); - equal(convert(["1", "2"], optionWithType(ParameterType.String)), "1,2"); - equal(convert(null, optionWithType(ParameterType.String)), ""); - equal(convert(void 0, optionWithType(ParameterType.String)), ""); + equal(convert("123", optionWithType(ParameterType.String), ""), "123"); + equal(convert(123, optionWithType(ParameterType.String), ""), "123"); + equal( + convert(["1", "2"], optionWithType(ParameterType.String), ""), + "1,2" + ); + equal(convert(null, optionWithType(ParameterType.String), ""), ""); + equal(convert(void 0, optionWithType(ParameterType.String), ""), ""); }); it("Validates string options", () => { @@ -115,26 +151,50 @@ describe("Options - Default convert function", () => { } }, }; - equal(convert("TOASTY", declaration), "TOASTY"); + equal(convert("TOASTY", declaration, ""), "TOASTY"); throws( - () => convert("toasty", declaration), + () => convert("toasty", declaration, ""), new Error("test must be upper case") ); }); it("Converts to booleans", () => { - equal(convert("a", optionWithType(ParameterType.Boolean)), true); - equal(convert([1], optionWithType(ParameterType.Boolean)), true); - equal(convert(false, optionWithType(ParameterType.Boolean)), false); + equal(convert("a", optionWithType(ParameterType.Boolean), ""), true); + equal(convert([1], optionWithType(ParameterType.Boolean), ""), true); + equal(convert(false, optionWithType(ParameterType.Boolean), ""), false); }); it("Converts to arrays", () => { - equal(convert("12,3", optionWithType(ParameterType.Array)), [ - "12", - "3", + equal(convert("12,3", optionWithType(ParameterType.Array), ""), [ + "12,3", + ]); + equal(convert(["12,3"], optionWithType(ParameterType.Array), ""), [ + "12,3", + ]); + equal(convert(true, optionWithType(ParameterType.Array), ""), []); + + equal(convert("/,a", optionWithType(ParameterType.PathArray), ""), [ + resolve("/,a"), + ]); + equal(convert(["/foo"], optionWithType(ParameterType.PathArray), ""), [ + resolve("/foo"), + ]); + equal(convert(true, optionWithType(ParameterType.PathArray), ""), []); + + equal(convert("a,b", optionWithType(ParameterType.ModuleArray), ""), [ + "a,b", + ]); + equal(convert(["a,b"], optionWithType(ParameterType.ModuleArray), ""), [ + "a,b", ]); - equal(convert(["12,3"], optionWithType(ParameterType.Array)), ["12,3"]); - equal(convert(true, optionWithType(ParameterType.Array)), []); + equal(convert(true, optionWithType(ParameterType.ModuleArray), ""), []); + }); + + it("ModuleArray is resolved if relative", () => { + equal( + convert(["./foo"], optionWithType(ParameterType.ModuleArray), ""), + [join(process.cwd(), "foo")] + ); }); it("Validates array options", () => { @@ -148,10 +208,10 @@ describe("Options - Default convert function", () => { } }, }; - equal(convert(["1"], declaration), ["1"]); - equal(convert(["1", "2"], declaration), ["1", "2"]); + equal(convert(["1"], declaration, ""), ["1"]); + equal(convert(["1", "2"], declaration, ""), ["1", "2"]); throws( - () => convert([], declaration), + () => convert([], declaration, ""), new Error("test must not be empty") ); }); @@ -167,9 +227,9 @@ describe("Options - Default convert function", () => { }, defaultValue: 1, }; - equal(convert("a", declaration), 1); - equal(convert("b", declaration), 2); - equal(convert(2, declaration), 2); + equal(convert("a", declaration, ""), 1); + equal(convert("b", declaration, ""), 2); + equal(convert(2, declaration, ""), 2); }); it("Converts to mapped types with a map", () => { @@ -183,9 +243,9 @@ describe("Options - Default convert function", () => { ]), defaultValue: 1, }; - equal(convert("a", declaration), 1); - equal(convert("b", declaration), 2); - equal(convert(2, declaration), 2); + equal(convert("a", declaration, ""), 1); + equal(convert("b", declaration, ""), 2); + equal(convert(2, declaration, ""), 2); }); it("Uses the mapError if provided for errors", () => { @@ -198,7 +258,7 @@ describe("Options - Default convert function", () => { mapError: "Test error", }; throws( - () => convert("a", declaration), + () => convert("a", declaration, ""), new Error(declaration.mapError) ); }); @@ -215,7 +275,7 @@ describe("Options - Default convert function", () => { defaultValue: 1, }; throws( - () => convert("c", declaration), + () => convert("c", declaration, ""), new Error("test must be one of a, b") ); }); @@ -233,14 +293,14 @@ describe("Options - Default convert function", () => { defaultValue: Enum.a, } as const; throws( - () => convert("c", declaration), + () => convert("c", declaration, ""), new Error("test must be one of a, b") ); }); it("Passes through mixed", () => { const data = Symbol(); - equal(convert(data, optionWithType(ParameterType.Mixed)), data); + equal(convert(data, optionWithType(ParameterType.Mixed), ""), data); }); it("Validates mixed options", () => { @@ -255,10 +315,136 @@ describe("Options - Default convert function", () => { } }, }; - equal(convert("text", declaration), "text"); + equal(convert("text", declaration, ""), "text"); throws( - () => convert(1, declaration), + () => convert(1, declaration, ""), new Error("test must not be a number") ); }); }); + +describe("Options - default values", () => { + function getDeclaration( + type: ParameterType, + defaultValue: unknown + ): DeclarationOption { + return { + type, + defaultValue, + name: "test", + help: "", + } as DeclarationOption; + } + + it("String", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.String, void 0)), + "" + ); + equal( + getDefaultValue(getDeclaration(ParameterType.String, "foo")), + "foo" + ); + }); + + it("Path", () => { + equal(getDefaultValue(getDeclaration(ParameterType.Path, void 0)), ""); + equal( + getDefaultValue(getDeclaration(ParameterType.Path, "foo")), + resolve("foo") + ); + }); + + it("Number", () => { + equal(getDefaultValue(getDeclaration(ParameterType.Number, void 0)), 0); + equal(getDefaultValue(getDeclaration(ParameterType.Number, 123)), 123); + ok( + Number.isNaN( + getDefaultValue(getDeclaration(ParameterType.Number, NaN)) + ) + ); + }); + + it("Boolean", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.Boolean, void 0)), + false + ); + equal( + getDefaultValue(getDeclaration(ParameterType.Boolean, true)), + true + ); + }); + + it("Map", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.Map, void 0)), + void 0 + ); + const def = {}; + ok(getDefaultValue(getDeclaration(ParameterType.Map, def)) === def); + }); + + it("Mixed", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.Mixed, void 0)), + void 0 + ); + const def = {}; + ok(getDefaultValue(getDeclaration(ParameterType.Mixed, def)) === def); + }); + + it("Array", () => { + equal(getDefaultValue(getDeclaration(ParameterType.Array, void 0)), []); + equal(getDefaultValue(getDeclaration(ParameterType.Array, ["a"])), [ + "a", + ]); + }); + + it("PathArray", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.PathArray, void 0)), + [] + ); + equal(getDefaultValue(getDeclaration(ParameterType.PathArray, ["a"])), [ + resolve("a"), + ]); + equal( + getDefaultValue(getDeclaration(ParameterType.PathArray, ["/a"])), + [resolve("/a")] + ); + }); + + it("ModuleArray", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.ModuleArray, void 0)), + [] + ); + equal( + getDefaultValue(getDeclaration(ParameterType.ModuleArray, ["a"])), + ["a"] + ); + equal( + getDefaultValue(getDeclaration(ParameterType.ModuleArray, ["./a"])), + [resolve("./a")] + ); + }); + + it("GlobArray", () => { + equal( + getDefaultValue(getDeclaration(ParameterType.GlobArray, void 0)), + [] + ); + equal(getDefaultValue(getDeclaration(ParameterType.GlobArray, ["a"])), [ + resolve("a"), + ]); + equal( + getDefaultValue(getDeclaration(ParameterType.GlobArray, ["**a"])), + ["**a"] + ); + equal( + getDefaultValue(getDeclaration(ParameterType.GlobArray, ["#!a"])), + ["#!" + resolve("a")] + ); + }); +}); diff --git a/src/test/utils/options/readers/arguments.test.ts b/src/test/utils/options/readers/arguments.test.ts index ccfb7cf34..02398133c 100644 --- a/src/test/utils/options/readers/arguments.test.ts +++ b/src/test/utils/options/readers/arguments.test.ts @@ -7,6 +7,7 @@ import { NumberDeclarationOption, MapDeclarationOption, } from "../../../../lib/utils/options"; +import { join, resolve } from "path"; describe("Options - ArgumentsReader", () => { // Note: We lie about the type of Options here since we want the less strict @@ -52,11 +53,14 @@ describe("Options - ArgumentsReader", () => { } test("Puts arguments with no flag into inputFiles", ["foo", "bar"], () => { - equal(options.getValue("entryPoints"), ["foo", "bar"]); + equal(options.getValue("entryPoints"), [ + join(process.cwd(), "foo"), + join(process.cwd(), "bar"), + ]); }); test("Works with string options", ["--out", "outDir"], () => { - equal(options.getValue("out"), "outDir"); + equal(options.getValue("out"), join(process.cwd(), "outDir")); }); test("Works with number options", ["-numOption", "123"], () => { @@ -90,7 +94,9 @@ describe("Options - ArgumentsReader", () => { ["--includeVersion", "foo"], () => { equal(options.getValue("includeVersion"), true); - equal(options.getValue("entryPoints"), ["foo"]); + equal(options.getValue("entryPoints"), [ + join(process.cwd(), "foo"), + ]); } ); @@ -103,22 +109,14 @@ describe("Options - ArgumentsReader", () => { }); test("Works with array options", ["--exclude", "a"], () => { - equal(options.getValue("exclude"), ["a"]); + equal(options.getValue("exclude"), [resolve("a")]); }); - test( - "Splits array options (backward compatibility)", - ["--exclude", "a,b"], - () => { - equal(options.getValue("exclude"), ["a", "b"]); - } - ); - test( "Works with array options passed multiple times", ["--exclude", "a", "--exclude", "b"], () => { - equal(options.getValue("exclude"), ["a", "b"]); + equal(options.getValue("exclude"), [resolve("a"), resolve("b")]); } ); diff --git a/src/test/utils/paths.test.ts b/src/test/utils/paths.test.ts deleted file mode 100644 index ac08b09d0..000000000 --- a/src/test/utils/paths.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as Path from "path"; - -import isEqual = require("lodash/isEqual"); -import Assert = require("assert"); - -import { createMinimatch } from "../../lib/utils/paths"; - -// Used to ensure uniform path cross OS -const absolutePath = (path: string) => - Path.resolve(path.replace(/^\w:/, "")).replace(/[\\]/g, "/"); - -describe("Paths", () => { - describe("createMinimatch", () => { - it("Minimatch can match absolute paths expressions", () => { - const paths = [ - "/unix/absolute/**/path", - "\\windows\\alternative\\absolute\\path", - "\\Windows\\absolute\\*\\path", - "**/arbitrary/path/**", - ]; - const mms = createMinimatch(paths); - const patterns = mms.map(({ pattern }) => pattern); - const comparePaths = [ - absolutePath("/unix/absolute/**/path"), - absolutePath("/windows/alternative/absolute/path"), - absolutePath("/Windows/absolute/*/path"), - "**/arbitrary/path/**", - ]; - - Assert(isEqual(patterns, comparePaths)); - - Assert( - mms[0].match(absolutePath("/unix/absolute/some/sub/dir/path")) - ); - Assert( - mms[1].match(absolutePath("/windows/alternative/absolute/path")) - ); - Assert(mms[2].match(absolutePath("/Windows/absolute/test/path"))); - Assert( - mms[3].match( - absolutePath("/some/deep/arbitrary/path/leading/nowhere") - ) - ); - }); - - it("Minimatch can match relative to the project root", () => { - const paths = [ - "./relative/**/path", - "../parent/*/path", - "no/dot/relative/**/path/*", - "*/subdir/**/path/*", - ]; - const absPaths = paths.map((path) => absolutePath(path)); - const mms = createMinimatch(paths); - const patterns = mms.map(({ pattern }) => pattern); - - Assert(isEqual(patterns, absPaths)); - Assert(mms[0].match(Path.resolve("relative/some/sub/dir/path"))); - Assert(mms[1].match(Path.resolve("../parent/dir/path"))); - Assert( - mms[2].match( - Path.resolve("no/dot/relative/some/sub/dir/path/test") - ) - ); - Assert(mms[3].match(Path.resolve("some/subdir/path/here"))); - }); - - it("Minimatch matches dot files", () => { - const mm = createMinimatch(["/some/path/**"])[0]; - Assert(mm.match(absolutePath("/some/path/.dot/dir"))); - Assert(mm.match(absolutePath("/some/path/normal/dir"))); - }); - - it("Minimatch matches negated expressions", () => { - const paths = ["!./some/path", "!!./some/path"]; - const mms = createMinimatch(paths); - - Assert( - !mms[0].match(Path.resolve("some/path")), - "Matched a negated expression" - ); - Assert( - mms[1].match(Path.resolve("some/path")), - "Didn't match a doubly negated expression" - ); - }); - - it("Minimatch does not match commented expressions", () => { - const [mm] = createMinimatch(["#/some/path"]); - - Assert(!mm.match("#/some/path"), "Matched a commented expression"); - }); - }); -});