diff --git a/packages/typedoc-plugin-code-blocks/src/plugin.spec.ts b/packages/typedoc-plugin-code-blocks/src/plugin.spec.ts index c231dc4d..5e799be3 100644 --- a/packages/typedoc-plugin-code-blocks/src/plugin.spec.ts +++ b/packages/typedoc-plugin-code-blocks/src/plugin.spec.ts @@ -47,7 +47,7 @@ describe( 'Behavior', () => { }, } ); } ); - it( 'should not affect text if no code block', () => { + it.only( 'should not affect text if no code block', () => { const text = 'Hello world' ; expect( markdownReplacerTestbed.runMarkdownReplace( text ) ).toEqual( text ); } ); diff --git a/packages/typedoc-plugin-code-blocks/src/plugin.ts b/packages/typedoc-plugin-code-blocks/src/plugin.ts index 16f28667..e28f9403 100644 --- a/packages/typedoc-plugin-code-blocks/src/plugin.ts +++ b/packages/typedoc-plugin-code-blocks/src/plugin.ts @@ -2,9 +2,9 @@ import assert from 'assert'; import { relative } from 'path'; import { once } from 'lodash'; -import { Application, JSX, RendererEvent } from 'typedoc'; +import { Application, JSX, LogLevel } from 'typedoc'; -import { ABasePlugin, CurrentPageMemo, MarkdownReplacer, PathReflectionResolver } from '@knodes/typedoc-pluginutils'; +import { ABasePlugin, CurrentPageMemo, EventsExtra, MarkdownReplacer, PathReflectionResolver } from '@knodes/typedoc-pluginutils'; import { getCodeBlockRenderer } from './code-blocks'; import { DEFAULT_BLOCK_NAME, ICodeSample, readCodeSample } from './code-sample-file'; @@ -33,10 +33,14 @@ export class CodeBlockPlugin extends ABasePlugin { * @see {@link import('@knodes/typedoc-pluginutils').autoload}. */ public initialize(): void { - this.application.renderer.on( RendererEvent.BEGIN, this._codeBlockRenderer.bind( this ) ); // Hook over each markdown events to replace code blocks - this._markdownReplacer.bindReplace( EXTRACT_CODE_BLOCKS_REGEX, this._replaceCodeBlock.bind( this ) ); + this._markdownReplacer.bindReplace( EXTRACT_CODE_BLOCKS_REGEX, this._replaceCodeBlock.bind( this ), 'replace code blocks' ); this._currentPageMemo.initialize(); + EventsExtra.for( this.application ) + .onThemeReady( this._codeBlockRenderer.bind( this ) ) + .onSetOption( `${this.optionsPrefix}:logLevel`, v => { + this.logger.level = v as LogLevel; + } ); } @@ -74,7 +78,7 @@ export class CodeBlockPlugin extends ABasePlugin { this.logger.error( () => `In "${sourceHint()}", could not resolve file "${file}" from ${this._currentPageMemo.currentReflection.name}` ); return fullMatch; } else { - this.logger.verbose( () => `Created a code block to ${resolvedFile} from "${sourceHint()}"` ); + this.logger.verbose( () => `Created a code block to ${this.relativeToRoot( resolvedFile )} from "${sourceHint()}"` ); } if( !this._fileSamples.has( resolvedFile ) ){ this._fileSamples.set( resolvedFile, readCodeSample( resolvedFile ) ); diff --git a/packages/typedoc-plugin-pages/src/plugin.ts b/packages/typedoc-plugin-pages/src/plugin.ts index 2b3b40e0..a85558fc 100644 --- a/packages/typedoc-plugin-pages/src/plugin.ts +++ b/packages/typedoc-plugin-pages/src/plugin.ts @@ -1,9 +1,9 @@ import assert from 'assert'; import { isString, once } from 'lodash'; -import { Application, JSX, RendererEvent } from 'typedoc'; +import { Application, JSX, LogLevel, RendererEvent } from 'typedoc'; -import { ABasePlugin, CurrentPageMemo, MarkdownReplacer, PathReflectionResolver } from '@knodes/typedoc-pluginutils'; +import { ABasePlugin, CurrentPageMemo, EventsExtra, MarkdownReplacer, PathReflectionResolver } from '@knodes/typedoc-pluginutils'; import { buildOptions } from './options'; import { NodeReflection } from './reflections'; @@ -26,10 +26,19 @@ export class PagesPlugin extends ABasePlugin { public override initialize(){ const opts = this.pluginOptions.getValue(); this.logger.level = opts.logLevel; - this._markdownReplacer.bindReplace( EXTRACT_PAGE_LINK_REGEX, this._replacePageLink.bind( this ) ); this._currentPageMemo.initialize(); - this.application.renderer.on( RendererEvent.BEGIN, this._pageTreeBuilder.bind( this ) ); - this.application.renderer.on( RendererEvent.BEGIN, this.addPagesToProject.bind( this ) ); + this.application.renderer.on( RendererEvent.BEGIN, this._addPagesToProject.bind( this ) ); + + EventsExtra.for( this.application ) + .beforeOptionsFreeze( () => { + if( this.pluginOptions.getValue().enablePageLinks ){ + this._markdownReplacer.bindReplace( EXTRACT_PAGE_LINK_REGEX, this._replacePageLink.bind( this ), 'replace page links' ); + } + } ) + .onThemeReady( this._pageTreeBuilder.bind( this ) ) + .onSetOption( `${this.optionsPrefix}:logLevel`, v => { + this.logger.level = v as LogLevel; + } ); } /** @@ -37,7 +46,7 @@ export class PagesPlugin extends ABasePlugin { * * @param event - The renderer event emitted at {@link RendererEvent.BEGIN}. */ - public addPagesToProject( event: RendererEvent ){ + private _addPagesToProject( event: RendererEvent ){ const opts = this.pluginOptions.getValue(); this._pageTreeBuilder().appendToProject( event, opts ); this.application.logger.info( `Generating ${this._pageTreeBuilder().mappings.length} pages` ); @@ -79,7 +88,7 @@ export class PagesPlugin extends ABasePlugin { .map( m => this.relativeToRoot( m.model.sourceFilePath ) ) )}` ); return fullMatch; } else { - this.logger.verbose( () => `Created a link from "${sourceHint()}" to "${mapping.model.name}" (resolved as "${resolvedFile}")` ); + this.logger.verbose( () => `Created a link from "${sourceHint()}" to "${mapping.model.name}" (resolved as "${this.relativeToRoot( resolvedFile )}")` ); } const link = builder.renderPageLink( { label: label ?? undefined, mapping } ); if( typeof link === 'string' ){ diff --git a/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts.patch b/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts.patch index 59c9f0d2..28ab01c9 100644 --- a/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts.patch +++ b/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts.patch @@ -1,5 +1,5 @@ diff --git a/typedoc/src/lib/output/plugins/JavascriptIndexPlugin.ts b/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts -index 8770e6fc..7f310a75 100755 +index 8770e6fc..c6598447 100755 --- a/typedoc/src/lib/output/plugins/JavascriptIndexPlugin.ts +++ b/packages/typedoc-plugin-pages/src/theme-plugins/search/default-pages-javascript-index-plugin.GENERATED.ts @@ -1,29 +1,65 @@ @@ -67,7 +67,7 @@ index 8770e6fc..7f310a75 100755 + if( !( reflection instanceof ANodeReflection ) ){ + return { reflection, isPage: false }; + } -+ if( reflection instanceof MenuReflection ){ ++ if( !this._plugin.pluginOptions.getValue().enableSearch || reflection instanceof MenuReflection ){ + return { reflection: null, isPage: false }; + } + const name = [ @@ -83,7 +83,7 @@ index 8770e6fc..7f310a75 100755 } /** -@@ -31,10 +67,7 @@ export class JavascriptIndexPlugin extends RendererComponent { +@@ -31,29 +67,27 @@ export class JavascriptIndexPlugin extends RendererComponent { * * @param event - An event object describing the current render operation. */ @@ -95,7 +95,8 @@ index 8770e6fc..7f310a75 100755 if ( event.isDefaultPrevented ) { return; } -@@ -42,18 +75,18 @@ export class JavascriptIndexPlugin extends RendererComponent { + ++ this._plugin.logger.verbose( `${this._plugin.pluginOptions.getValue().enableSearch ? 'Enabling' : 'Disabling'} search for pages` ); const rows: any[] = []; const kinds: { [K in ReflectionKind]?: string } = {}; @@ -120,7 +121,7 @@ index 8770e6fc..7f310a75 100755 ) { continue; } -@@ -69,6 +102,7 @@ export class JavascriptIndexPlugin extends RendererComponent { +@@ -69,6 +103,7 @@ export class JavascriptIndexPlugin extends RendererComponent { name: reflection.name, url: reflection.url, classes: reflection.cssClasses, @@ -128,7 +129,7 @@ index 8770e6fc..7f310a75 100755 }; if ( parent ) { -@@ -76,8 +110,8 @@ export class JavascriptIndexPlugin extends RendererComponent { +@@ -76,8 +111,8 @@ export class JavascriptIndexPlugin extends RendererComponent { } if ( !kinds[reflection.kind] ) { @@ -139,7 +140,7 @@ index 8770e6fc..7f310a75 100755 ); } -@@ -91,14 +125,14 @@ export class JavascriptIndexPlugin extends RendererComponent { +@@ -91,14 +126,14 @@ export class JavascriptIndexPlugin extends RendererComponent { builder.field( 'name', { boost: 10 } ); builder.field( 'parent' ); @@ -157,7 +158,7 @@ index 8770e6fc..7f310a75 100755 ); const jsonData = JSON.stringify( { kinds, -@@ -106,9 +140,10 @@ export class JavascriptIndexPlugin extends RendererComponent { +@@ -106,9 +141,10 @@ export class JavascriptIndexPlugin extends RendererComponent { index, } ); diff --git a/packages/typedoc-plugintestbed/src/mock-markdown-replacer.ts b/packages/typedoc-plugintestbed/src/mock-markdown-replacer.ts index 1a324fa0..566cd3ca 100644 --- a/packages/typedoc-plugintestbed/src/mock-markdown-replacer.ts +++ b/packages/typedoc-plugintestbed/src/mock-markdown-replacer.ts @@ -1,5 +1,6 @@ import assert from 'assert'; +import { isNil } from 'lodash'; import { MarkdownEvent, Renderer } from 'typedoc'; import { setupCaptureEvent } from './capture-event'; @@ -8,11 +9,12 @@ export const setupMockMarkdownReplacer = () => { const capture = setupCaptureEvent( Renderer, MarkdownEvent.PARSE ); return { captureEventRegistration: capture.captureEventRegistration, - runMarkdownReplace: ( text: string ) => { + runMarkdownReplace: ( text: string, listenerIndex?: number ) => { const listeners = capture.getListeners(); - assert.equal( listeners.length, 1, `Invalid listeners count for event ${MarkdownEvent.PARSE}` ); const markdownEvent = new MarkdownEvent( MarkdownEvent.PARSE, text, text ); - listeners[0]( markdownEvent ); + assert( listeners.length >= 1, `Invalid listeners count for event ${MarkdownEvent.PARSE}` ); + const listenersToTrigger = isNil( listenerIndex ) ? listeners : [ listeners[listenerIndex] ]; + listenersToTrigger.forEach( l => l( markdownEvent ) ); return markdownEvent.parsedText; }, }; diff --git a/packages/typedoc-plugintestbed/src/mock-page-memo.ts b/packages/typedoc-plugintestbed/src/mock-page-memo.ts index 817f4b2f..a3d9306b 100644 --- a/packages/typedoc-plugintestbed/src/mock-page-memo.ts +++ b/packages/typedoc-plugintestbed/src/mock-page-memo.ts @@ -1,6 +1,7 @@ import assert from 'assert'; -import { MarkdownEvent, PageEvent, ProjectReflection, Reflection, RenderTemplate, Renderer, SourceFile } from 'typedoc'; +import { isNil } from 'lodash'; +import { PageEvent, ProjectReflection, Reflection, RenderTemplate, Renderer, SourceFile } from 'typedoc'; import { setupCaptureEvent } from './capture-event'; @@ -8,21 +9,29 @@ export const setupMockPageMemo = () => { const capture = setupCaptureEvent( Renderer, Renderer.EVENT_BEGIN_PAGE ); return { captureEventRegistration: capture.captureEventRegistration, - setCurrentPage: ( url: string, source: string, model: T, template: RenderTemplate> = () => '', project = new ProjectReflection( 'Fake' ) ) => { + setCurrentPage: ( + url: string, + source: string, + model: T, + template: RenderTemplate> = () => '', + project = new ProjectReflection( 'Fake' ), + listenerIndex?: number, + ) => { const listeners = capture.getListeners(); - assert.equal( listeners.length, 1, `Invalid listeners count for event ${MarkdownEvent.PARSE}` ); - const event = new PageEvent( PageEvent.BEGIN ); - event.project = project; - event.url = url; - event.model = model; - event.template = template ?? ( () => '' ); - event.filename = url; + assert( listeners.length >= 1, `Invalid listeners count for event ${Renderer.EVENT_BEGIN_PAGE}` ); + const pageEvent = new PageEvent( PageEvent.BEGIN ); + pageEvent.project = project; + pageEvent.url = url; + pageEvent.model = model; + pageEvent.template = template ?? ( () => '' ); + pageEvent.filename = url; Object.defineProperty( model, 'project', { value: project } ); model.sources = [ ...( model.sources ?? [] ), { fileName: source, character: 1, line: 1, file: new SourceFile( source ) }, ]; - listeners[0]( event ); + const listenersToTrigger = isNil( listenerIndex ) ? listeners : [ listeners[listenerIndex] ]; + listenersToTrigger.forEach( l => l( pageEvent ) ); }, }; }; diff --git a/packages/typedoc-pluginutils/src/current-page-memo.ts b/packages/typedoc-pluginutils/src/current-page-memo.ts index 2353bc97..d70e1de4 100644 --- a/packages/typedoc-pluginutils/src/current-page-memo.ts +++ b/packages/typedoc-pluginutils/src/current-page-memo.ts @@ -8,14 +8,21 @@ import { ABasePlugin } from './base-plugin'; export class CurrentPageMemo { private _currentPage?: PageEvent; + private _initialized = false; + public get initialized(){ + return this._initialized; + } public constructor( protected readonly plugin: ABasePlugin ){} - /** * Start watching for pages event. */ public initialize(){ + if( this._initialized ){ + return; + } + this._initialized = true; this.plugin.application.renderer.on( Renderer.EVENT_BEGIN_PAGE, ( e: PageEvent ) => { this._currentPage = e; } ); diff --git a/packages/typedoc-pluginutils/src/events-extra.ts b/packages/typedoc-pluginutils/src/events-extra.ts new file mode 100644 index 00000000..7f87bb5d --- /dev/null +++ b/packages/typedoc-pluginutils/src/events-extra.ts @@ -0,0 +1,113 @@ +import assert from 'assert'; + +import { Application } from 'typedoc'; + +type Fn = ( ...args: any[] ) => any; +type MethodKeys = {[k in keyof T]: T[k] extends Fn ? k : never}[keyof T] & string +type Params = T extends Fn ? Parameters : unknown[]; +type Ret = T extends Fn ? ReturnType : unknown; +export class EventsExtra { + private static readonly _apps = new WeakMap(); + + /** + * Get events extra for the given application. + * + * @param application - The application to bind. + * @returns the events extra instance. + */ + public static for( application: Application ){ + const e = this._apps.get( application ) ?? new EventsExtra( application ); + this._apps.set( application, e ); + return e; + } + + private constructor( private readonly application: Application ){} + + /** + * Execute a function after the option {@link name} has been set. + * + * @param name - The option name to watch. + * @param cb - The function to execute. + * @returns this. + */ + public onSetOption( name: string, cb: ( value: unknown ) => void ){ + // eslint-disable-next-line @typescript-eslint/dot-notation -- Private property + this._hookInstanceAfter( this.application.options['_setOptions'] as Set, 'add', ( set, v ) => { + if( v === name ){ + cb( this.application.options.getValue( name ) ); + } + return set; + } ); + return this; + } + + /** + * Execute a function just after theme have been set. + * + * @param cb - The function to execute. + * @returns this. + */ + public onThemeReady( cb: () => void ){ + this._hookInstanceAfter( this.application.renderer, 'prepareTheme' as any, ( success: boolean ) => { + if( success ){ + cb(); + } + return success; + } ); + return this; + } + + /** + * Execute a function just before options freezing. + * + * @param cb - The function to execute. + * @returns this. + */ + public beforeOptionsFreeze( cb: () => void ){ + this._hookInstanceBefore( this.application.options, 'freeze', ( ...args ) => { + cb(); + return args; + } ); + return this; + } + + /** + * Replace the method {@link key} of {@link instance} with a method calling the original method, then the custom {@link hook}. + * The original method return value is passed as the 1st parameter of the hook. + * + * @param instance - The instance to bind. + * @param key - The method name. + * @param hook - The function to execute after the original one. + */ + private _hookInstanceAfter< + T extends {}, // eslint-disable-line @typescript-eslint/ban-types -- Inspired from jest `spyOn` types. + K extends MethodKeys, + >( instance: T, key: K, hook: ( initialRet: Ret, ...args: Params ) => Ret ){ + const bck = ( instance[key] as T[K] & Fn ).bind( instance ) as T[K] & Fn; + assert( bck ); + ( instance[key] as any ) = ( ...args: Params ) => { + const ret = bck( ...args ); + return hook( ret, ...args ); + }; + } + + /** + * Replace the method {@link key} of {@link instance} with a method calling the the custom {@link hook}, then the original method. + * The hook should return arguments to pass to the original method. + * + * @param instance - The instance to bind. + * @param key - The method name. + * @param hook - The function to execute before the original one. + */ + private _hookInstanceBefore< + T extends {}, // eslint-disable-line @typescript-eslint/ban-types -- Inspired from jest `spyOn` types. + K extends MethodKeys, + >( instance: T, key: K, hook: ( ...args: Params ) => Params ){ + const bck = ( instance[key] as T[K] & Fn ).bind( instance ) as T[K] & Fn; + assert( bck ); + ( instance[key] as any ) = ( ...args: Params ) => { + const newArgs = hook( ...args ); + return bck( ...newArgs ); + }; + } +} diff --git a/packages/typedoc-pluginutils/src/index.ts b/packages/typedoc-pluginutils/src/index.ts index 55c1f4b5..f39f5dc0 100644 --- a/packages/typedoc-pluginutils/src/index.ts +++ b/packages/typedoc-pluginutils/src/index.ts @@ -1,6 +1,7 @@ export * from './autoload'; export * from './base-plugin'; export * from './current-page-memo'; +export * from './events-extra'; export * from './markdown-replacer'; export * from './options'; export * from './path-reflection-resolver'; diff --git a/packages/typedoc-pluginutils/src/markdown-replacer.ts b/packages/typedoc-pluginutils/src/markdown-replacer.ts index 40935ba5..e49f8452 100644 --- a/packages/typedoc-pluginutils/src/markdown-replacer.ts +++ b/packages/typedoc-pluginutils/src/markdown-replacer.ts @@ -14,17 +14,17 @@ interface ISourceEdit { replacement: string; } interface ISourceMapContainer { - editions: ISourceEdit[]; - getEditionContext: ( position: number ) => ( { + readonly editions: ISourceEdit[]; + readonly getEditionContext: ( position: number ) => ( { line: number; column: number; expansions: ISourceMapContainer[]; index: number; source: string; } ); - label: string; - plugin: ABasePlugin; - regex: RegExp; + readonly label: string; + readonly plugin: ABasePlugin; + readonly regex: RegExp; } const spitArgs = ( ...args: Parameters[1]> ) => { const indexIdx = args.findIndex( isNumber ); @@ -101,6 +101,7 @@ export class MarkdownReplacer { */ public bindReplace( regex: RegExp, callback: MarkdownReplacer.ReplaceCallback, label = `${this.plugin.name}: Unnamed markdown replace` ) { assert( regex.flags.includes( 'g' ) ); + this._currentPageMemo.initialize(); this.plugin.application.renderer.on( MarkdownEvent.PARSE, this._processMarkdown.bind( this, regex, callback, label ), undefined, 100 ); } @@ -123,38 +124,15 @@ export class MarkdownReplacer { const sourceFile = this._currentPageMemo.hasCurrent ? reflectionSourceUtils.getReflectionSourceFileName( this._currentPageMemo.currentReflection ) : undefined; const relativeSource = sourceFile ? this.plugin.relativeToRoot( sourceFile ) : undefined; const originalText = event.parsedText; - const edits: ISourceEdit[] = []; const getCtxInParent = last( mapContainers )?.getEditionContext ?? ( pos => ( { ...textUtils.getCoordinates( originalText, pos ), source: originalText, index: pos, expansions: [] } ) ); - event.parsedText = originalText.replace( - regex, - ( ...args ) => { - const { captures, fullMatch, index } = spitArgs( ...args ); - const replacement = callback( - { fullMatch, captures }, - () => { - if( !relativeSource ){ - return 'UNKNOWN SOURCE'; - } - const { line, column, expansions } = getCtxInParent( index ); - const posStr = line && column ? `:${line}:${column}` : ''; - const expansionContext = ` (in expansion of ${expansions.concat( [ thisContainer ] ).map( e => e.label ).join( ' ⇒ ' )})`; - return relativeSource + posStr + expansionContext; - } ); - if( isNil( replacement ) ){ - return fullMatch; - } - const replacementStr = typeof replacement === 'string' ? replacement : JSX.renderElement( replacement ); - edits.push( { from: index, to: index + fullMatch.length, replacement: replacementStr, source: fullMatch } ); - return replacementStr; - } ); const thisContainer: ISourceMapContainer = { regex, - editions: edits, + editions: [], label, plugin: this.plugin, getEditionContext: ( pos: number ) => { - const { offsetedPos, didEdit } = edits.reduce( + const { offsetedPos, didEdit } = thisContainer.editions.reduce( ( acc, edit ) => { const isAfterEdit = edit.from <= acc.offsetedPos; if( !isAfterEdit ){ @@ -176,6 +154,28 @@ export class MarkdownReplacer { return parentCtx; }, }; + event.parsedText = originalText.replace( + regex, + ( ...args ) => { + const { captures, fullMatch, index } = spitArgs( ...args ); + const replacement = callback( + { fullMatch, captures }, + () => { + if( !relativeSource ){ + return 'UNKNOWN SOURCE'; + } + const { line, column, expansions } = getCtxInParent( index ); + const posStr = line && column ? `:${line}:${column}` : ''; + const expansionContext = ` (in expansion of ${expansions.concat( [ thisContainer ] ).map( e => e.label ).join( ' ⇒ ' )})`; + return relativeSource + posStr + expansionContext; + } ); + if( isNil( replacement ) ){ + return fullMatch; + } + const replacementStr = typeof replacement === 'string' ? replacement : JSX.renderElement( replacement ); + thisContainer.editions.push( { from: index, to: index + fullMatch.length, replacement: replacementStr, source: fullMatch } ); + return replacementStr; + } ); MarkdownReplacer._mapContainers.set( event, [ ...mapContainers, thisContainer, diff --git a/packages/typedoc-pluginutils/src/options/option-group.spec.ts b/packages/typedoc-pluginutils/src/options/option-group.spec.ts index 6f2d1329..694e418e 100644 --- a/packages/typedoc-pluginutils/src/options/option-group.spec.ts +++ b/packages/typedoc-pluginutils/src/options/option-group.spec.ts @@ -116,7 +116,7 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 0, 'TEST:bar': '', 'TEST': { foo: 0, bar: '' }} ); expect( opts.getValue() ).toMatchObject( { foo: 0, bar: '' } ); } ); @@ -125,8 +125,8 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); opts.setValue( { bar: 'Hello', foo: 42 } ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 42, 'TEST:bar': 'Hello', 'TEST': { foo: 42, bar: 'Hello' }} ); expect( opts.getValue() ).toMatchObject( { foo: 42, bar: 'Hello' } ); } ); @@ -135,8 +135,8 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); opts.setValue( { bar: 'Hello' } ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 0, 'TEST:bar': 'Hello', 'TEST': { foo: 0, bar: 'Hello' }} ); expect( opts.getValue() ).toMatchObject( { foo: 0, bar: 'Hello' } ); } ); @@ -145,8 +145,8 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); opts.setValue( JSON.stringify( { bar: 'World' } ) ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 0, 'TEST:bar': 'World', 'TEST': { foo: 0, bar: 'World' }} ); expect( opts.getValue() ).toMatchObject( { foo: 0, bar: 'World' } ); } ); @@ -157,8 +157,8 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); opts.setValue( tmp.name ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 0, 'TEST:bar': 'Doh', 'TEST': { foo: 0, bar: 'Doh' }} ); expect( opts.getValue() ).toMatchObject( { foo: 0, bar: 'Doh' } ); } ); @@ -169,8 +169,8 @@ describe( 'Behavior', () => { .add( 'bar', { type: ParameterType.String, help: '' } ) .add( 'foo', { type: ParameterType.Number, help: '' } ) .build(); - app.options.read( app.logger ); opts.setValue( tmp.name ); + app.options.freeze(); expect( app.options.getRawValues() ).toMatchObject( { 'TEST:foo': 0, 'TEST:bar': 'DohJS', 'TEST': { foo: 0, bar: 'DohJS' }} ); expect( opts.getValue() ).toMatchObject( { foo: 0, bar: 'DohJS' } ); } ); diff --git a/packages/typedoc-pluginutils/src/options/option-group.ts b/packages/typedoc-pluginutils/src/options/option-group.ts index 4e304b6d..efdbe4f8 100644 --- a/packages/typedoc-pluginutils/src/options/option-group.ts +++ b/packages/typedoc-pluginutils/src/options/option-group.ts @@ -5,6 +5,7 @@ import { defaultsDeep, get, identity, kebabCase } from 'lodash'; import { DeclarationOption, ParameterType } from 'typedoc'; import type { ABasePlugin } from '../base-plugin'; +import { EventsExtra } from '../events-extra'; import { MapperPart, Option } from './option'; import { DecOptType, TypeErr, _DecOpt } from './utils'; @@ -85,20 +86,19 @@ export class OptionGroup< help: `[${this.plugin.package.name}]: Set all plugin options below as an object, a JSON string or from a file.${linkAppendix}`, } ); - const readBck = this.plugin.application.options.read.bind( this.plugin.application.options ); - this.plugin.application.options.read = logger => { - readBck( logger ); - const defaultOpts = this.getValue(); - // Try read default files - const generalOpts = this.plugin.application.options.getValue( this.plugin.optionsPrefix ) ?? `./typedoc-${kebabCase( this.plugin.optionsPrefix )}` as any; - if( generalOpts ){ - try { - this._setValue( generalOpts ); - // eslint-disable-next-line no-empty -- Autoload errors allowed. - } catch( _e ){} - } - this.setValue( defaultsDeep( this.getValue(), defaultOpts ) ); - }; + EventsExtra.for( this.plugin.application ) + .beforeOptionsFreeze( () => { + const defaultOpts = this.getValue(); + // Try read default files + const generalOpts = this.plugin.application.options.getValue( this.plugin.optionsPrefix ) ?? `./typedoc-${kebabCase( this.plugin.optionsPrefix )}` as any; + if( generalOpts ){ + try { + this._setValue( generalOpts ); + // eslint-disable-next-line no-empty -- Autoload errors allowed. + } catch( _e ){} + } + this.setValue( defaultsDeep( this.getValue(), defaultOpts ) ); + } ); } /** @@ -167,6 +167,11 @@ export class OptionGroup< } return o.getValue(); } ); + for( const k in value ){ + if( !( k in newOpts ) ){ + this.plugin.application.options.setValue( `${this.plugin.optionsPrefix}:${k}`, value[k] ); + } + } this.plugin.application.options.setValue( this.plugin.optionsPrefix, newOpts ); } }