diff --git a/CHANGELOG.md b/CHANGELOG.md index 142da3ceb..f27b12e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Removed `Reflection#kindString`, use `ReflectionKind.singularString(reflection.kind)` or `ReflectionKind.pluralString(reflection.kind)` instead. - Properties related to rendering are no longer stored on `Reflection`, including `url`, `anchor`, `hasOwnDocument`, and `cssClasses`. - Removed special case for `--plugin none`, to disable plugin auto discovery, set `"plugin": []` in a config file. +- `Application.bootstrap` is now async to support ESM based plugins, #1635. # Unreleased diff --git a/bin/typedoc b/bin/typedoc index 81745b55c..0b4c18818 100755 --- a/bin/typedoc +++ b/bin/typedoc @@ -23,8 +23,6 @@ app.options.addReader(new td.TypeDocReader()); app.options.addReader(new td.TSConfigReader()); app.options.addReader(new td.ArgumentsReader(300)); -app.bootstrap(); - run(app) .catch((error) => { console.error("TypeDoc exiting with unexpected error:"); @@ -35,6 +33,8 @@ run(app) /** @param {td.Application} app */ async function run(app) { + await app.bootstrap(); + if (app.options.getValue("version")) { console.log(app.toString()); return ExitCodes.Ok; diff --git a/src/lib/application.ts b/src/lib/application.ts index d4d7282d2..719bb7a6e 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -116,7 +116,7 @@ export class Application extends ChildableComponent< * * @param options The desired options to set. */ - bootstrap(options: Partial = {}): void { + async bootstrap(options: Partial = {}): Promise { for (const [key, val] of Object.entries(options)) { try { this.options.setValue(key as keyof TypeDocOptions, val); @@ -137,7 +137,7 @@ export class Application extends ChildableComponent< this.logger.level = this.options.getValue("logLevel"); const plugins = discoverPlugins(this); - loadPlugins(this, plugins); + await loadPlugins(this, plugins); this.options.reset(); for (const [key, val] of Object.entries(options)) { diff --git a/src/lib/converter/factories/index-signature.ts b/src/lib/converter/factories/index-signature.ts index 96f170010..41ea0837f 100644 --- a/src/lib/converter/factories/index-signature.ts +++ b/src/lib/converter/factories/index-signature.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import assert from "assert"; import * as ts from "typescript"; import { DeclarationReflection, diff --git a/src/lib/converter/factories/signature.ts b/src/lib/converter/factories/signature.ts index 00aefd594..de456238c 100644 --- a/src/lib/converter/factories/signature.ts +++ b/src/lib/converter/factories/signature.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import * as assert from "assert"; +import assert from "assert"; import { DeclarationReflection, IntrinsicType, diff --git a/src/lib/converter/symbols.ts b/src/lib/converter/symbols.ts index 75939ae9f..d484665cd 100644 --- a/src/lib/converter/symbols.ts +++ b/src/lib/converter/symbols.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import assert from "assert"; import * as ts from "typescript"; import { DeclarationReflection, diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 927f3e152..6daa74648 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import assert from "assert"; import * as ts from "typescript"; import { ArrayType, diff --git a/src/lib/utils/plugins.ts b/src/lib/utils/plugins.ts index b8e1fea39..af03a7b1b 100644 --- a/src/lib/utils/plugins.ts +++ b/src/lib/utils/plugins.ts @@ -1,21 +1,39 @@ import * as FS from "fs"; import * as Path from "path"; +import { isAbsolute } from "path"; import type { Application } from "../application"; import type { Logger } from "./loggers"; import { nicePath } from "./paths"; -export function loadPlugins(app: Application, plugins: readonly string[]) { +export async function loadPlugins( + app: Application, + plugins: readonly string[] +) { for (const plugin of plugins) { const pluginDisplay = getPluginDisplayName(plugin); try { // eslint-disable-next-line @typescript-eslint/no-var-requires - const instance = require(plugin); + let instance: any; + try { + instance = require(plugin); + } catch (error: any) { + if (error.code === "ERR_REQUIRE_ESM") { + // On Windows, we need to ensure this path is a file path. + // Or we'll get ERR_UNSUPPORTED_ESM_URL_SCHEME + const esmPath = isAbsolute(plugin) + ? `file:///${plugin}` + : plugin; + instance = await import(esmPath); + } else { + throw error; + } + } const initFunction = instance.load; if (typeof initFunction === "function") { - initFunction(app); + await initFunction(app); app.logger.info(`Loaded plugin ${pluginDisplay}`); } else { app.logger.error( diff --git a/src/test/slow/visual.test.ts b/src/test/slow/visual.test.ts index 8b8fd998f..927da9ef7 100644 --- a/src/test/slow/visual.test.ts +++ b/src/test/slow/visual.test.ts @@ -1,5 +1,7 @@ import { deepStrictEqual as equal, ok } from "assert"; -import { RegSuitCore } from "reg-suit-core"; +// Using a require here to avoid loading types since they're busted. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { RegSuitCore } = require("reg-suit-core"); import { captureRegressionScreenshots } from "../capture-screenshots"; describe("Visual Test", () => { diff --git a/src/test/utils/plugins.test.ts b/src/test/utils/plugins.test.ts new file mode 100644 index 000000000..f018e4c75 --- /dev/null +++ b/src/test/utils/plugins.test.ts @@ -0,0 +1,93 @@ +import { Project, tempdirProject } from "@typestrong/fs-fixture-builder"; +import type { Application } from "../../index"; +import { loadPlugins } from "../../lib/utils/plugins"; +import { TestLogger } from "../TestLogger"; +import { join, resolve } from "path"; + +describe("loadPlugins", () => { + let project: Project; + let logger: TestLogger; + const fakeApp = {} as any as Application; + beforeEach(() => { + project = tempdirProject(); + logger = fakeApp.logger = new TestLogger(); + }); + + afterEach(() => { + project.rm(); + }); + + it("Should support loading a basic plugin", async () => { + project.addJsonFile("package.json", { + type: "commonjs", + main: "index.js", + }); + project.addFile("index.js", "exports.load = function load() {}"); + project.write(); + + const plugin = resolve(project.cwd); + await loadPlugins(fakeApp, [plugin]); + logger.expectMessage(`info: Loaded plugin ${plugin}`); + }); + + it("Should support loading a ESM plugin", async () => { + project.addJsonFile("package.json", { + type: "module", + main: "index.js", + }); + project.addFile("index.js", "export function load() {}"); + project.write(); + + const plugin = join(resolve(project.cwd), "index.js"); + await loadPlugins(fakeApp, [plugin]); + logger.expectMessage(`info: Loaded plugin ${plugin}`); + }); + + it("Should handle errors when requiring plugins", async () => { + project.addJsonFile("package.json", { + type: "commonjs", + main: "index.js", + }); + project.addFile("index.js", "throw Error('bad')"); + project.write(); + + const plugin = join(resolve(project.cwd), "index.js"); + await loadPlugins(fakeApp, [plugin]); + logger.expectMessage( + `error: The plugin ${plugin} could not be loaded.` + ); + }); + + it("Should handle errors when loading plugins", async () => { + project.addJsonFile("package.json", { + type: "commonjs", + main: "index.js", + }); + project.addFile( + "index.js", + "exports.load = function load() { throw Error('bad') }" + ); + project.write(); + + const plugin = join(resolve(project.cwd), "index.js"); + await loadPlugins(fakeApp, [plugin]); + logger.expectMessage( + `error: The plugin ${plugin} could not be loaded.` + ); + }); + + it("Should handle plugins without a load method", async () => { + project.addJsonFile("package.json", { + type: "commonjs", + main: "index.js", + }); + project.addFile("index.js", ""); + project.write(); + + const plugin = join(resolve(project.cwd), "index.js"); + await loadPlugins(fakeApp, [plugin]); + logger.expectMessage( + `error: Invalid structure in plugin ${plugin}, no load function found.` + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index bdccdc0eb..966eae6b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "CommonJS", + "module": "Node16", "lib": ["es2020"], "target": "es2020",