diff --git a/package-lock.json b/package-lock.json index 44d6d6700..9f11cebea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -465,12 +465,6 @@ "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", "dev": true }, - "@types/mockery": { - "version": "1.4.29", - "resolved": "https://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz", - "integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=", - "dev": true - }, "@types/node": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.1.tgz", @@ -1903,12 +1897,6 @@ } } }, - "mockery": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mockery/-/mockery-2.1.0.tgz", - "integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 33afde0be..54cff97df 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,11 @@ "@types/marked": "^2.0.2", "@types/minimatch": "3.0.4", "@types/mocha": "^8.2.2", - "@types/mockery": "^1.4.29", "@types/node": "^15.0.1", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", "eslint": "^7.25.0", "mocha": "^8.3.2", - "mockery": "^2.1.0", "nyc": "^15.1.0", "prettier": "^2.2.1", "typescript": "^4.2.4" diff --git a/src/lib/application.ts b/src/lib/application.ts index 094936aef..53bbb05f7 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -10,9 +10,11 @@ import { Logger, ConsoleLogger, CallbackLogger, - PluginHost, + loadPlugins, normalizePath, writeFile, + discoverNpmPlugins, + NeverIfInternal, } from "./utils/index"; import { createMinimatch } from "./utils/paths"; @@ -78,8 +80,6 @@ export class Application extends ChildableComponent< options: Options; - plugins: PluginHost; - @BindOption("logger") loggerType!: string | Function; @@ -114,7 +114,6 @@ export class Application extends ChildableComponent< this.serializer = new Serializer(); this.converter = this.addComponent("converter", Converter); this.renderer = this.addComponent("renderer", Renderer); - this.plugins = this.addComponent("plugins", PluginHost); } /** @@ -142,7 +141,11 @@ export class Application extends ChildableComponent< } this.logger.level = this.options.getValue("logLevel"); - this.plugins.load(); + let plugins = this.options.getValue("plugin"); + if (plugins.length === 0) { + plugins = discoverNpmPlugins(this); + } + loadPlugins(this, this.options.getValue("plugin")); this.options.reset(); for (const [key, val] of Object.entries(options)) { @@ -158,8 +161,11 @@ export class Application extends ChildableComponent< /** * Return the application / root component instance. */ - get application(): Application { - return this; + get application(): NeverIfInternal { + this.logger.deprecated( + "Application.application is deprecated. Plugins are now passed the application instance when loaded." + ); + return this as never; } /** @@ -204,9 +210,9 @@ export class Application extends ChildableComponent< const programs = [ ts.createProgram({ - rootNames: this.application.options.getFileNames(), - options: this.application.options.getCompilerOptions(), - projectReferences: this.application.options.getProjectReferences(), + rootNames: this.options.getFileNames(), + options: this.options.getCompilerOptions(), + projectReferences: this.options.getProjectReferences(), }), ]; @@ -238,7 +244,7 @@ export class Application extends ChildableComponent< return; } - if (this.application.options.getValue("emit")) { + if (this.options.getValue("emit")) { for (const program of programs) { program.emit(); } @@ -284,7 +290,7 @@ export class Application extends ChildableComponent< // Doing this is considerably more complicated, we'd need to manage an array of programs, not convert until all programs // have reported in the first time... just error out for now. I'm not convinced anyone will actually notice. - if (this.application.options.getFileNames().length === 0) { + if (this.options.getFileNames().length === 0) { this.logger.error( "The provided tsconfig file looks like a solution style tsconfig, which is not supported in watch mode." ); @@ -308,7 +314,7 @@ export class Application extends ChildableComponent< const host = ts.createWatchCompilerHost( tsconfigFile, - { noEmit: !this.application.options.getValue("emit") }, + { noEmit: !this.options.getValue("emit") }, ts.sys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, (diagnostic) => this.logger.diagnostic(diagnostic), @@ -401,7 +407,7 @@ export class Application extends ChildableComponent< end: eventData, }); - const space = this.application.options.getValue("pretty") ? "\t" : ""; + const space = this.options.getValue("pretty") ? "\t" : ""; await writeFile(out, JSON.stringify(ser, null, space)); this.logger.info(`JSON written to ${out}`); } diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index e07ec33b2..f0bae9e13 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -861,8 +861,8 @@ const tupleConverter: TypeConverter = { ); return new TupleType(elements); }, - convertType(context, type) { - const types = type.typeArguments?.slice(0, type.target.fixedLength); + convertType(context, type, node) { + const types = type.typeArguments?.slice(0, node.elements.length); let elements = types?.map((type) => convertType(context, type)); if (type.target.labeledElementDeclarations) { diff --git a/src/lib/utils/component.ts b/src/lib/utils/component.ts index 03f322c11..72dec7c4e 100644 --- a/src/lib/utils/component.ts +++ b/src/lib/utils/component.ts @@ -172,9 +172,17 @@ export abstract class AbstractComponent * Return the application / root component instance. */ get application(): Application { - return this._componentOwner === DUMMY_APPLICATION_OWNER - ? ((this as any) as Application) - : this._componentOwner.application; + if (this._componentOwner === DUMMY_APPLICATION_OWNER) { + return (this as any) as Application; + } + // Temporary hack, Application.application is going away. + if ( + this._componentOwner instanceof AbstractComponent && + this._componentOwner._componentOwner === DUMMY_APPLICATION_OWNER + ) { + return (this._componentOwner as any) as Application; + } + return this._componentOwner.application; } /** diff --git a/src/lib/utils/fs.ts b/src/lib/utils/fs.ts index f827407de..cffa4b495 100644 --- a/src/lib/utils/fs.ts +++ b/src/lib/utils/fs.ts @@ -140,7 +140,8 @@ export async function remove(target: string) { // Since v14.14 if (fsp.rm) { await fsp.rm(target, { recursive: true, force: true }); - } else { + } else if (fs.existsSync(target)) { + // Ew. We shouldn't need the exists check... Can't wait for Node 14. await fsp.rmdir(target, { recursive: true }); } } diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 5252d396e..5f8fe8499 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -22,4 +22,4 @@ export { remove, } from "./fs"; export { Logger, LogLevel, ConsoleLogger, CallbackLogger } from "./loggers"; -export { PluginHost } from "./plugins"; +export { loadPlugins, discoverNpmPlugins } from "./plugins"; diff --git a/src/lib/utils/plugins.ts b/src/lib/utils/plugins.ts index d1181d2ce..186ce21d1 100644 --- a/src/lib/utils/plugins.ts +++ b/src/lib/utils/plugins.ts @@ -1,173 +1,131 @@ import * as FS from "fs"; import * as Path from "path"; -import { Application } from "../application"; -import { AbstractComponent, Component } from "./component"; -import { BindOption } from "./options"; -import { readFile } from "./fs"; +import type { Application } from "../application"; +import type { Logger } from "./loggers"; -/** - * Responsible for discovering and loading plugins. - */ -@Component({ name: "plugin-host", internal: true }) -export class PluginHost extends AbstractComponent { - @BindOption("plugin") - plugins!: string[]; - - /** - * Load all npm plugins. - * @returns TRUE on success, otherwise FALSE. - */ - load(): boolean { - const logger = this.application.logger; - const plugins = this.plugins.length - ? this.resolvePluginPaths(this.plugins) - : this.discoverNpmPlugins(); +export function loadPlugins(app: Application, plugins: readonly string[]) { + for (const plugin of resolvePluginPaths(plugins)) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const instance = require(plugin); + let initFunction = instance.load; - if (plugins.some((plugin) => plugin.toLowerCase() === "none")) { - return true; - } + if ( + typeof initFunction !== "function" && + typeof instance === "function" + ) { + app.logger.deprecated( + `${plugin} uses a deprecated structure. Plugins should export a "load" function to be called with the Application` + ); + initFunction = instance; + } - for (const plugin of plugins) { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const instance = require(plugin); - const initFunction = - typeof instance.load === "function" - ? instance.load - : instance; // support legacy plugins - if (typeof initFunction === "function") { - initFunction(this); - logger.info(`Loaded plugin ${plugin}`); - } else { - logger.error( - `Invalid structure in plugin ${plugin}, no function found.` - ); - } - } catch (error) { - logger.error(`The plugin ${plugin} could not be loaded.`); - logger.verbose(error.stack); - return false; + if (typeof initFunction === "function") { + initFunction(app); + app.logger.info(`Loaded plugin ${plugin}`); + } else { + app.logger.error( + `Invalid structure in plugin ${plugin}, no load function found.` + ); } + } catch (error) { + app.logger.error(`The plugin ${plugin} could not be loaded.`); + app.logger.error(error.stack); } - return true; } +} + +export function discoverNpmPlugins(app: Application): string[] { + const result: string[] = []; + discover(); + return result; /** - * Discover all installed TypeDoc plugins. - * - * @returns A list of all npm module names that are qualified TypeDoc plugins. + * Find all parent folders containing a `node_modules` subdirectory. */ - private discoverNpmPlugins(): string[] { - const result: string[] = []; - const logger = this.application.logger; - discover(); - return result; - - /** - * Find all parent folders containing a `node_modules` subdirectory. - */ - function discover() { - let path = process.cwd(), - previous: string; - do { - const modules = Path.join(path, "node_modules"); - if ( - FS.existsSync(modules) && - FS.statSync(modules).isDirectory() - ) { - discoverModules(modules); - } + function discover() { + let path = process.cwd(); + let previous: string; - previous = path; - path = Path.resolve(Path.join(previous, "..")); - } while (previous !== path); - } - - /** - * Scan the given `node_modules` directory for TypeDoc plugins. - */ - function discoverModules(basePath: string) { - const candidates: string[] = []; - FS.readdirSync(basePath).forEach((name) => { - const dir = Path.join(basePath, name); - if (name.startsWith("@") && FS.statSync(dir).isDirectory()) { - FS.readdirSync(dir).forEach((n) => { - candidates.push(Path.join(name, n)); - }); - } - candidates.push(name); - }); - candidates.forEach((name) => { - const infoFile = Path.join(basePath, name, "package.json"); - if (!FS.existsSync(infoFile)) { - return; - } + do { + const modules = Path.join(path, "node_modules"); + if (FS.existsSync(modules) && FS.statSync(modules).isDirectory()) { + discoverModules(modules); + } - const info = loadPackageInfo(infoFile); - if (isPlugin(info)) { - result.push(Path.join(basePath, name)); - } - }); - } + previous = path; + path = Path.resolve(Path.join(previous, "..")); + } while (previous !== path); + } - /** - * Load and parse the given `package.json`. - */ - function loadPackageInfo(fileName: string): any { - try { - return JSON.parse(readFile(fileName)); - } catch (error) { - logger.error(`Could not parse ${fileName}`); - return {}; + /** + * Scan the given `node_modules` directory for TypeDoc plugins. + */ + function discoverModules(basePath: string) { + const candidates: string[] = []; + FS.readdirSync(basePath).forEach((name) => { + const dir = Path.join(basePath, name); + if (name.startsWith("@") && FS.statSync(dir).isDirectory()) { + FS.readdirSync(dir).forEach((n) => { + candidates.push(Path.join(name, n)); + }); } - } - - /** - * Test whether the given package info describes a TypeDoc plugin. - */ - function isPlugin(info: any): boolean { - const keywords: unknown[] = info.keywords; - if (!keywords || !Array.isArray(keywords)) { - return false; + candidates.push(name); + }); + candidates.forEach((name) => { + const infoFile = Path.join(basePath, name, "package.json"); + if (!FS.existsSync(infoFile)) { + return; } - for (let i = 0, c = keywords.length; i < c; i++) { - const keyword = keywords[i]; - if ( - typeof keyword === "string" && - keyword.toLowerCase() === "typedocplugin" - ) { - return true; - } + const info = loadPackageInfo(app.logger, infoFile); + if (isPlugin(info)) { + result.push(Path.join(basePath, name)); } + }); + } +} - return false; - } +/** + * Load and parse the given `package.json`. + */ +function loadPackageInfo(logger: Logger, fileName: string): any { + try { + return require(fileName); + } catch { + logger.error(`Could not parse ${fileName}`); + return {}; } +} - /** - * Resolves plugin paths to absolute paths from the current working directory - * (`process.cwd()`). - * - * ```txt - * ./plugin -> resolve - * ../plugin -> resolve - * plugin -> don't resolve (module resolution) - * /plugin -> don't resolve (already absolute path) - * c:\plugin -> don't resolve (already absolute path) - * ``` - * - * @param plugins - */ - private resolvePluginPaths(plugins: 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; - }); +/** + * Test whether the given package info describes a TypeDoc plugin. + */ +function isPlugin(info: any): boolean { + if (typeof info !== "object" || !info) { + return false; + } + + const keywords: unknown[] = info.keywords; + if (!keywords || !Array.isArray(keywords)) { + return false; } + + return keywords.some( + (keyword) => + typeof keyword === "string" && + 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/plugin-host.test.ts b/src/test/plugin-host.test.ts deleted file mode 100644 index 87f1be091..000000000 --- a/src/test/plugin-host.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Application } from ".."; -import Assert = require("assert"); -import * as mockery from "mockery"; -import * as path from "path"; - -describe("PluginHost", function () { - before(function () { - mockery.enable({ - warnOnReplace: false, - warnOnUnregistered: false, - }); - mockery.registerMock("typedoc-plugin-1", () => { - // nop - }); - mockery.registerMock("typedoc-plugin-2", () => { - // nop - }); - }); - - after(function () { - mockery.disable(); - }); - - it("parses plugins correctly", function () { - const app = new Application(); - app.bootstrap({ - plugin: ["typedoc-plugin-1", "typedoc-plugin-2"], - }); - - Assert.deepEqual(app.plugins.plugins, [ - "typedoc-plugin-1", - "typedoc-plugin-2", - ]); - }); - - it("loads a plugin with relative path", function () { - const app = new Application(); - app.bootstrap({ - plugin: ["./dist/test/plugins/relative"], - }); - - Assert.deepEqual(app.plugins.plugins, ["./dist/test/plugins/relative"]); - }); - - it("loads a plugin with absolute path", function () { - const app = new Application(); - const absolutePath = path.resolve(__dirname, "./plugins/absolute"); - app.bootstrap({ - plugin: [absolutePath], - }); - - Assert.deepEqual(app.plugins.plugins, [absolutePath]); - }); -});