From 022e34bc8f4909d0cb33a062af8abfc240bce817 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Sun, 24 Jul 2022 22:40:17 +0200 Subject: [PATCH 01/31] feat(plugin-pages): implement logic for glob expansion Related to #132 --- package-lock.json | 49 ++++--- package.json | 3 +- packages/plugin-pages/package.json | 4 +- .../src/converter/page-tree/expand-context.ts | 37 +++++ .../src/converter/page-tree/index.ts | 1 + .../page-tree/page-tree-builder.spec.ts | 131 +++++++++++++++++- .../converter/page-tree/page-tree-builder.ts | 89 +++++++++++- packages/plugin-pages/src/index.ts | 1 + packages/plugin-pages/src/options/types.ts | 21 ++- packages/plugintestbed/src/mock-plugin.ts | 7 +- packages/pluginutils/src/reflection-paths.ts | 8 +- 11 files changed, 316 insertions(+), 35 deletions(-) create mode 100644 packages/plugin-pages/src/converter/page-tree/expand-context.ts diff --git a/package-lock.json b/package-lock.json index 6a3b2c49..f15cd94f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/concurrently": "^7.0.0", "@types/diff": "^5.0.2", "@types/fs-extra": "^9.0.13", + "@types/glob": "^7.2.0", "@types/jest": "^28.1.6", "@types/js-beautify": "^1.13.3", "@types/jsdom": "^20.0.0", @@ -1888,6 +1889,16 @@ "@types/node": "*" } }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -4862,8 +4873,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -5118,7 +5128,6 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5508,7 +5517,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5517,8 +5525,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -7635,7 +7642,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -9638,8 +9644,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.1", @@ -9871,12 +9876,14 @@ "license": "MIT", "dependencies": { "@knodes/typedoc-pluginutils": "~0.23.1", + "glob": "^8.0.3", "lodash": "^4.17.21" }, "devDependencies": { "@knodes/eslint-config": "^1.6.5", "@testing-library/jest-dom": "^5.16.4", "@types/fs-extra": "^9.0.13", + "@types/glob": "^7.2.0", "@types/jest": "^28.1.6", "@types/jsdom": "^20.0.0", "@types/lodash": "^4.14.182", @@ -11297,6 +11304,7 @@ "@knodes/typedoc-pluginutils": "~0.23.1", "@testing-library/jest-dom": "^5.16.4", "@types/fs-extra": "^9.0.13", + "@types/glob": "^7.2.0", "@types/jest": "^28.1.6", "@types/jsdom": "^20.0.0", "@types/lodash": "^4.14.182", @@ -11312,6 +11320,7 @@ "eslint-plugin-jsdoc": "^39.3.3", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-sort-export-all": "^1.2.2", + "glob": "^8.0.3", "jest": "^28.1.1", "jest-extended": "^3.0.2", "jest-junit": "^14.0.0", @@ -11571,6 +11580,16 @@ "@types/node": "*" } }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -13852,8 +13871,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.2", @@ -14045,7 +14063,6 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14313,7 +14330,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -14322,8 +14338,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -15936,7 +15951,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -17438,8 +17452,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { "version": "4.0.1", diff --git a/package.json b/package.json index a026ebcc..71a9c195 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "typescript": "^4.7.4", "unionfs": "^4.4.0", "yaml": "^2.1.1", - "fastest-levenshtein": "^1.0.16" + "fastest-levenshtein": "^1.0.16", + "@types/glob": "^7.2.0" } } diff --git a/packages/plugin-pages/package.json b/packages/plugin-pages/package.json index b8c83772..386bca1c 100644 --- a/packages/plugin-pages/package.json +++ b/packages/plugin-pages/package.json @@ -40,7 +40,8 @@ }, "dependencies": { "lodash": "^4.17.21", - "@knodes/typedoc-pluginutils": "~0.23.1" + "@knodes/typedoc-pluginutils": "~0.23.1", + "glob": "^8.0.3" }, "peerDependencies": { "typedoc": "^0.23.0", @@ -71,6 +72,7 @@ "typedoc": "^0.23.10", "typescript": "^4.7.4", "@types/fs-extra": "^9.0.13", + "@types/glob": "^7.2.0", "@types/lunr": "^2.3.4", "conventional-changelog-cli": "^2.2.2", "type-fest": "^2.16.0" diff --git a/packages/plugin-pages/src/converter/page-tree/expand-context.ts b/packages/plugin-pages/src/converter/page-tree/expand-context.ts new file mode 100644 index 00000000..e19c97eb --- /dev/null +++ b/packages/plugin-pages/src/converter/page-tree/expand-context.ts @@ -0,0 +1,37 @@ +import { basename, dirname, relative } from 'path'; + +import { LoDashStatic, cloneDeep, isArray, isPlainObject, isString, template } from 'lodash'; +import { normalizePath } from 'typedoc'; + +export interface IExpandContext { + from: string; + match: string; + fullPath: string; + prev: IExpandContext[]; +} + +export interface IExpandContextImports{ + _: LoDashStatic; + path: Pick; +} +const imports: Omit = { + path: { + dirname: ( ...args: Parameters ) => normalizePath( dirname( ...args ) ), + basename: ( ...args: Parameters ) => normalizePath( basename( ...args ) ), + relative: ( ...args: Parameters ) => normalizePath( relative( ...args ) ), + }, +}; + +export const expandNode = ( node: T, context: IExpandContext ): T => { + const nodeClone = cloneDeep( node ) as any; + Object.entries( nodeClone ).forEach( ( [ k, v ] ) => { + if( isString( v ) ){ + nodeClone[k] = template( v, { variable: 'context', imports } )( context ); + } else if( isArray( v ) ){ + nodeClone[k] = v.map( vv => expandNode( vv, context ) ); + } else if( isPlainObject( v ) ){ + nodeClone[k] = expandNode( v, context ); + } + } ); + return nodeClone; +}; diff --git a/packages/plugin-pages/src/converter/page-tree/index.ts b/packages/plugin-pages/src/converter/page-tree/index.ts index 6579a702..602973de 100644 --- a/packages/plugin-pages/src/converter/page-tree/index.ts +++ b/packages/plugin-pages/src/converter/page-tree/index.ts @@ -1,2 +1,3 @@ export * from './page-tree-builder'; export * from './utils'; +export { IExpandContextImports as ExpandContextImports, IExpandContext } from './expand-context'; diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts index 67f9581e..71260fb3 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts @@ -7,14 +7,14 @@ import { DeclarationReflection, LogLevel, ProjectReflection, Reflection, Reflect import { MockPlugin, createMockProjectWithPackage, mockPlugin, restoreFs, setVirtualFs } from '#plugintestbed'; import { MenuReflection, PageReflection } from '../../models/reflections'; -import { EInvalidPageLinkHandling, IPluginOptions, IRootPageNode } from '../../options'; +import { EInvalidPageLinkHandling, IPluginOptions } from '../../options'; import { PagesPlugin } from '../../plugin'; import { PageTreeBuilder } from './page-tree-builder'; let plugin: MockPlugin; let pageTreeBuilder: PageTreeBuilder; let project: ProjectReflection; -const opts = ( pages: IRootPageNode[] ) => ( { +const opts = ( pages: IPluginOptions['pages'] ) => ( { pages, enablePageLinks: true, enableSearch: true, @@ -25,14 +25,18 @@ const opts = ( pages: IRootPageNode[] ) => ( { } as IPluginOptions ); beforeEach( () => { plugin = mockPlugin(); + Object.assign( plugin.application.options.getRawValues(), { + entryPoints: [ normalizePath( process.cwd() ) ], + } ); pageTreeBuilder = new PageTreeBuilder( plugin ); project = createMockProjectWithPackage(); } ); +process.chdir( __dirname ); afterEach( restoreFs ); -const addChildModule = ( name: string ) => { +const addChildModule = ( name: string, path = name ) => { const moduleRef = new DeclarationReflection( name, ReflectionKind.Module, project ); moduleRef.sources = [ - new SourceReference( resolve( `${name}/index.ts` ), 0, 0 ), + new SourceReference( resolve( `${path}/index.ts` ), 0, 0 ), ]; project.children ??= []; project.children.push( moduleRef ); @@ -317,3 +321,122 @@ describe( 'Simple tree', () => { } ); } ); } ); +describe( 'Glob expansion', () => { + // See https://github.com/KnodesCommunity/typedoc-plugins/issues/132 + describe( 'String template interpolation', () => { + it( 'should expand properly pages', () => { + setVirtualFs( { + 'test-foo': { 'readme.md': 'Foo content' }, + 'test-bar': { 'readme.md': 'Bar content' }, + } ); + const out = pageTreeBuilder.buildPagesTree( project, opts( [ { match: '*/readme.md', template: [ + // eslint-disable-next-line no-template-curly-in-string + { name: '<%= _.startCase( path.dirname( context.match )) %>', source: '${ context.fullPath }' }, + ] } ] ) ); + expect( out.childrenNodes ).toEqual( [ + matchReflection( MenuReflection, { name: 'TEST', depth: 0, module: project, childrenNodes: [ + matchReflection( PageReflection, { name: 'Test Bar', content: 'Bar content', depth: 1, module: project } ), + matchReflection( PageReflection, { name: 'Test Foo', content: 'Foo content', depth: 1, module: project } ), + ] } ), + ] ); + } ); + it( 'should expand properly sub items', () => { + setVirtualFs( { + packages: { + foo: { 'readme.md': 'Foo content' }, + bar: { 'readme.md': 'Bar content' }, + }, + } ); + const out = pageTreeBuilder.buildPagesTree( project, opts( [ { match: 'packages/*', template: [ + // eslint-disable-next-line no-template-curly-in-string + { name: '<%= _.startCase( path.basename( context.match ) ) %>', childrenDir: '${context.match}', children: [ + // eslint-disable-next-line no-template-curly-in-string + { source: '${ context.fullPath }/readme.md', name: '<%= _.startCase( path.basename( context.match ) ) %> child' }, + ] }, + ] } ] ) ); + expect( out.childrenNodes ).toEqual( [ + matchReflection( MenuReflection, { name: 'TEST', depth: 0, module: project, childrenNodes: [ + matchReflection( MenuReflection, { name: 'Bar', depth: 1, module: project, childrenNodes: [ + matchReflection( PageReflection, { name: 'Bar child', content: 'Bar content', depth: 2, module: project } ), + ] } ), + matchReflection( MenuReflection, { name: 'Foo', depth: 1, module: project, childrenNodes: [ + matchReflection( PageReflection, { name: 'Foo child', content: 'Foo content', depth: 2, module: project } ), + ] } ), + ] } ), + ] ); + } ); + it( 'should expand properly nested items', () => { + setVirtualFs( { + packages: { + foo: { 'readme.md': 'Foo content', 'Changelog.md': 'Foo changelog' }, + bar: { 'readme.md': 'Bar content', 'Changelog.md': 'Bar changelog' }, + }, + } ); + const out = pageTreeBuilder.buildPagesTree( project, opts( [ { match: 'packages/*', template: [ + // eslint-disable-next-line no-template-curly-in-string + { name: '<%= _.startCase( path.basename( context.match ) ) %>', childrenDir: '${context.match}', children: [ + { match: '*.md', template: [ + // eslint-disable-next-line no-template-curly-in-string + { source: '${ context.fullPath }', name: '<%= _.startCase( path.basename( context.prev[0].match ) + " " + path.basename( context.match, ".md" ) ) %>' }, + ] }, + ] }, + ] } ] ) ); + expect( out.childrenNodes ).toEqual( [ + matchReflection( MenuReflection, { name: 'TEST', depth: 0, module: project, childrenNodes: [ + matchReflection( MenuReflection, { name: 'Bar', depth: 1, module: project, childrenNodes: [ + matchReflection( PageReflection, { name: 'Bar Changelog', content: 'Bar changelog', depth: 2, module: project } ), + matchReflection( PageReflection, { name: 'Bar Readme', content: 'Bar content', depth: 2, module: project } ), + ] } ), + matchReflection( MenuReflection, { name: 'Foo', depth: 1, module: project, childrenNodes: [ + matchReflection( PageReflection, { name: 'Foo Changelog', content: 'Foo changelog', depth: 2, module: project } ), + matchReflection( PageReflection, { name: 'Foo Readme', content: 'Foo content', depth: 2, module: project } ), + ] } ), + ] } ), + ] ); + } ); + it( 'should merge properly nested items', () => { + const targetModule = addChildModule( 'foo', 'packages/foo' ); + setVirtualFs( { + packages: { + foo: { + 'bar': { + 'readme.md': 'Foo content', + 'Changelog.md': 'Foo changelog', + }, + 'qux.md': 'Qux content', + }, + }, + } ); + const out = pageTreeBuilder.buildPagesTree( project, opts( [ + { match: 'packages/**/readme.md', template: [ + { name: '<%= _.lowerCase( path.relative("packages", context.match ).split("/")[0] ) %>', moduleRoot: true, children: [ + { name: 'Meta', children: [ + // eslint-disable-next-line no-template-curly-in-string + { source: '${ context.fullPath }', name: 'Readme' }, + ] }, + ] }, + ] }, + { match: 'packages/**/Changelog.md', template: [ + { name: '<%= _.lowerCase( path.relative("packages", context.match ).split("/")[0] ) %>', moduleRoot: true, children: [ + { name: 'Meta', children: [ + // eslint-disable-next-line no-template-curly-in-string + { source: '${ context.fullPath }', name: 'Changelog' }, + ] }, + ] }, + ] }, + { name: 'foo', moduleRoot: true, children: [ + { source: 'qux.md', name: 'Qux' }, + ] }, + ] ) ); + expect( out.childrenNodes ).toEqual( [ + matchReflection( MenuReflection, { name: 'foo', depth: 0, module: targetModule, childrenNodes: [ + matchReflection( MenuReflection, { name: 'Meta', depth: 1, module: targetModule, childrenNodes: [ + matchReflection( PageReflection, { name: 'Readme', content: 'Foo content', depth: 2, module: targetModule } ), + matchReflection( PageReflection, { name: 'Changelog', content: 'Foo changelog', depth: 2, module: targetModule } ), + ] } ), + matchReflection( PageReflection, { name: 'Qux', content: 'Qux content', depth: 1, module: targetModule } ), + ] } ), + ] ); + } ); + } ); +} ); diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts index 2104d949..79b3e564 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts @@ -1,12 +1,16 @@ import assert from 'assert'; +import { isAbsolute, resolve } from 'path'; +import { sync as glob } from 'glob'; +import { cloneDeep, uniq } from 'lodash'; import { DeclarationReflection, MinimalSourceFile, ProjectReflection, Reflection, normalizePath } from 'typedoc'; import { IPluginComponent, ResolveError, getWorkspaces, miscUtils, resolveNamedPath } from '@knodes/typedoc-pluginutils'; import { ANodeReflection, MenuReflection, NodeReflection, PageReflection } from '../../models/reflections'; -import { IPageNode, IPluginOptions, IRootPageNode } from '../../options'; +import { IOptionPatternPage, IPageNode, IPluginOptions, IRootPageNode, OptionsPageNode } from '../../options'; import type { PagesPlugin } from '../../plugin'; +import { IExpandContext, expandNode } from './expand-context'; import { getDir, getNodePath, getNodeUrl, join } from './utils'; const isModuleRoot = ( pageNode: IPageNode | IRootPageNode ) => 'moduleRoot' in pageNode && !!pageNode.moduleRoot; @@ -34,23 +38,98 @@ export class PageTreeBuilder implements IPluginComponent { if( !options.pages || options.pages.length === 0 ){ return rootMenu; } else { - if( options.pages.some( p => p.moduleRoot ) ){ + const pages = this._expandRootPageNodes( options.pages, options.source ); + if( pages.some( p => p.moduleRoot ) ){ rootMenu.childrenNodes = this._mapNodesToReflectionsTree( - options.pages, + pages, project, { inputContainer: options.source, output: options.output } ); } else { const projectRoot = new MenuReflection( project.name, project, project, project.url ?? '' ); projectRoot.childrenNodes = this._mapNodesToReflectionsTree( - options.pages, + pages, projectRoot, { inputContainer: options.source, output: options.output } ); rootMenu.childrenNodes = projectRoot.childrenNodes.length > 0 ? [ projectRoot ] : []; } + rootMenu.childrenNodes = this._dedupeNodes( rootMenu.childrenNodes ); return rootMenu; } } + /** + * Merge identical nodes (identical at this point means same name & module). + * + * @param nodes - The nodes to dedupe. + * @returns the deduped nodes. + */ + private _dedupeNodes( nodes: ANodeReflection[] ): ANodeReflection[] { + return nodes.reduce( ( acc, v ) => { + const existing = acc.find( a => a.module === v.module && a.name === v.name ); + if( existing ){ + if( existing instanceof PageReflection && v instanceof PageReflection ){ + throw new Error( `Deduping ${getNodePath( v )} failed: this page and ${getNodePath( existing )} both has source` ); + } + v.parent = existing; + existing.childrenNodes = this._dedupeNodes( uniq( [ + ...( existing.childrenNodes ?? [] ), + ...( v.childrenNodes ?? [] ).map( c => { + c.parent = existing; + return c; + } ), + ] ) ); + } else { + acc.push( v ); + } + return acc; + }, [] ); + } + + /** + * Expand each page entry for each entrypoint. + * + * @param pages - A list of pages options to expand. + * @param sourceDir - The pages container directory. + * @returns the expanded page nodes. + */ + private _expandRootPageNodes( pages: IPluginOptions.Page[], sourceDir: string ): IRootPageNode[]{ + const entryPoints = this.plugin.application.options.getValue( 'entryPoints' ).flatMap( ep => glob( ep ) ); + return pages.map( p => this._expandPageNode( entryPoints.map( ep => join( ep, sourceDir ) ), p, [] ) ).flat( 2 ); + } + + /** + * Expand the given node or node match from each if the given path sources. + * + * @param froms - A list of paths to try to expand against. + * @param node - The node to expand. + * @param prevContexts - A list of previous expansion contexts. + * @returns the expanded nodes. + */ + private _expandPageNode( froms: string[], node: OptionsPageNode | IOptionPatternPage, prevContexts: IExpandContext[] = [] ): T[] { + if( 'match' in node ){ + const matches = froms.flatMap( from => glob( node.match, { cwd: from } ).map( m => ( { + from, + match: normalizePath( m ), + fullPath: normalizePath( resolve( from, m ) ), + prev: prevContexts, + } as IExpandContext ) ) ); + const nodesExpanded = matches.map( m => node.template.map( t => { + const tClone = cloneDeep( t ); + if( 'children' in tClone ){ + tClone.children = tClone.children?.map( c => this._expandPageNode( [ m.fullPath ], c, [ ...prevContexts, m ] ) ).flat( 1 ) ?? []; + } + const expanded = expandNode( tClone, m ); + return expanded; + } ) ).flat( 1 ); + return nodesExpanded as any; + } + const clone = cloneDeep( node ); + if( 'children' in clone ){ + clone.children = clone.children?.map( c => this._expandPageNode( froms, c, [ ...prevContexts ] ) ).flat( 1 ) ?? []; + } + return [ clone as any ]; + } + /** * Get the module with the given {@link name}. * @@ -151,7 +230,7 @@ export class PageTreeBuilder implements IPluginComponent { private _getNodeReflection( node: PageNode, parent: ANodeReflection.Parent, io: IIOPath ){ const { module, parent: actualParent } = this._getNodeParent( node, parent ); if( node.source ){ - const nodePath = join( io.input, node.source ); + const nodePath = isAbsolute( node.source ) ? node.source : join( io.input, node.source ); const sourceFilePath = miscUtils.catchWrap( () => resolveNamedPath( module, io.inputContainer ?? undefined, nodePath ), err => { diff --git a/packages/plugin-pages/src/index.ts b/packages/plugin-pages/src/index.ts index 5636cae5..8d927b8e 100644 --- a/packages/plugin-pages/src/index.ts +++ b/packages/plugin-pages/src/index.ts @@ -2,3 +2,4 @@ export { load } from './load'; export { IPluginOptions, EInvalidPageLinkHandling, IPageNode, IRootPageNode } from './options'; export { PageReflection, MenuReflection, NodeReflection, PagesPluginReflectionKind, ANodeReflection } from './models/reflections'; export { IPagesPluginThemeMethods, IPagesPluginTheme, RenderPageLinkProps } from './output/theme/types'; +export { ExpandContextImports, IExpandContext } from './converter/page-tree'; diff --git a/packages/plugin-pages/src/options/types.ts b/packages/plugin-pages/src/options/types.ts index 589f68dd..c3942c92 100644 --- a/packages/plugin-pages/src/options/types.ts +++ b/packages/plugin-pages/src/options/types.ts @@ -61,6 +61,21 @@ export interface IRootPageNode extends IPageNode { moduleRoot?: boolean; } +export interface IOptionPatternPage { + match: string; + template: Array>; +} +export type OptionsPageNode = Omit & { + children?: Array; +} + +export interface IOptionsPage { + /** + * List of children nodes. Both pages & menu entries can have children. + */ + children?: IPageNode[]; +} + export enum EInvalidPageLinkHandling { FAIL = 'fail', LOG_ERROR = 'logError', @@ -76,7 +91,7 @@ export interface IPluginOptions { * * @see {@page pages-tree.md} for details. */ - pages: IRootPageNode[]; + pages: IPluginOptions.Page[]; /** * Whether or not @page and @pagelink tags should be parsed. @@ -125,4 +140,6 @@ export interface IPluginOptions { */ excludeMarkdownTags?: string[]; } - +export namespace IPluginOptions { + export type Page = OptionsPageNode | IOptionPatternPage; +} diff --git a/packages/plugintestbed/src/mock-plugin.ts b/packages/plugintestbed/src/mock-plugin.ts index e8545fc2..3e44a608 100644 --- a/packages/plugintestbed/src/mock-plugin.ts +++ b/packages/plugintestbed/src/mock-plugin.ts @@ -9,8 +9,8 @@ export type MockPlugin = jest.MockedObjectD export const mockPlugin = ( props: Partial> = {} ): MockPlugin => { const mockLogger = { makeChildLogger: jest.fn(), - error: jest.fn().mockImplementation( v => fail( `Unexpected error log: ${typeof v === 'function' ? v() : v}` ) ), - warn: jest.fn().mockImplementation( v => fail( `Unexpected warn log: ${typeof v === 'function' ? v() : v}` ) ), + error: jest.fn().mockImplementation( v => {throw new Error( `Unexpected error log: ${typeof v === 'function' ? v() : v}` ); } ), + warn: jest.fn().mockImplementation( v => {throw new Error( `Unexpected warn log: ${typeof v === 'function' ? v() : v}` ); } ), log: jest.fn(), verbose: jest.fn(), info: jest.fn(), @@ -23,7 +23,8 @@ export const mockPlugin = ( props: Partial< const application: any = { logger: { level: LogLevel.Verbose, log: jest.fn() }, options: { - getValue: jest.fn().mockImplementation( k => isNil( k ) ? opts : opts[k] ), + getValue: jest.fn().mockImplementation( k => opts[k] ), + getRawValues: jest.fn().mockImplementation( () => opts ), _setOptions: new Set(), }, }; diff --git a/packages/pluginutils/src/reflection-paths.ts b/packages/pluginutils/src/reflection-paths.ts index 4bb0be4c..4ed21421 100644 --- a/packages/pluginutils/src/reflection-paths.ts +++ b/packages/pluginutils/src/reflection-paths.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { existsSync, readdirSync } from 'fs'; -import { dirname, parse, resolve } from 'path'; +import { dirname, isAbsolute, parse, resolve } from 'path'; import { memoize } from 'lodash'; import { LiteralUnion } from 'type-fest'; @@ -107,6 +107,12 @@ export const resolveNamedPath: { const [ currentReflection, containerFolder, path ] = args.length === 3 ? args : [ args[0], undefined, args[1] ]; let containerFolderMut = containerFolder; let pathMut = normalizePath( path ); + if( isAbsolute( pathMut ) ){ + if( existsSync( pathMut ) ){ + return pathMut; + } + throw new Error( `Resolved file "${pathMut}" does not exists` ); + } let reflectionRoots = findModuleRoot( getReflectionModule( currentReflection ) ); if( pathMut.startsWith( '~~:' ) ){ pathMut = pathMut.slice( 3 ); From 62926aa73643da35a3fe1138062f6774063d5ee3 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Mon, 25 Jul 2022 01:02:23 +0200 Subject: [PATCH 02/31] feat(plugin-pages): add validation for glob patterns Related to #132 --- packages/plugin-pages/src/options/build.ts | 46 +++++++++++-------- .../plugin-pages/src/options/options.spec.ts | 36 +++++++++++++-- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/packages/plugin-pages/src/options/build.ts b/packages/plugin-pages/src/options/build.ts index 0f084652..c97d86c9 100644 --- a/packages/plugin-pages/src/options/build.ts +++ b/packages/plugin-pages/src/options/build.ts @@ -1,6 +1,6 @@ import assert, { AssertionError } from 'assert'; -import { difference, groupBy, isArray, isNil, isObject, isString, uniq } from 'lodash'; +import { difference, groupBy, isArray, isNil, isObject, isString } from 'lodash'; import { LogLevel, ParameterType } from 'typedoc'; import { OptionGroup, miscUtils } from '@knodes/typedoc-pluginutils'; @@ -20,20 +20,30 @@ const pageKeys: Array = [ 'children', 'childrenDir', 'childrenO const checkPageFactory = ( allowedKeys: Array ) => ( plugin: PagesPlugin, page: unknown, path: string[] ): asserts page is T => { assert( page && isObject( page ), 'Page should be an object' ); const _page = page as Record; - if( 'title' in _page && !( 'name' in _page ) ){ - _page.name = _page.title; - delete _page.title; - plugin.logger.warn( `Page ${[ ...path, _page.name ].map( p => `"${p}"` ).join( ' ⇒ ' )} is using deprecated "title" property. Use "name" instead.` ); - } - assert( 'name' in _page && isString( _page.name ), 'Page should have a name' ); - const extraProps = difference( Object.keys( _page ), allowedKeys as string[] ); - assert.equal( extraProps.length, 0, `Page ${[ ...path, _page.name ].map( p => `"${p}"` ).join( ' ⇒ ' )} have extra properties ${JSON.stringify( extraProps )}` ); - if( 'children' in _page && !isNil( _page.children ) ){ - assert( isArray( _page.children ), 'Page children should be an array' ); - const thisPath = [ ...path, _page.name as string ]; - _page.children.forEach( ( c, i ) => miscUtils.catchWrap( - () => checkPage( plugin, c, thisPath ), - wrapPageError( thisPath, i ) ) ); + const pagePath = () => [ ...path, _page.name ].map( p => `"${p ?? 'Unnamed'}"` ).join( ' ⇒ ' ); + if( 'match' in page && 'template' in page ){ + assert( isString( _page.match ) ); + assert( isArray( _page.template ) ); + const selfFn = checkPageFactory( allowedKeys ); + _page.template.forEach( ( t, i ) => selfFn( plugin, t, [ ...path, `TEMPLATE ${i + 1}` ] ) ); + } else if( !( 'match' in page ) && !( 'template' in page ) ){ + if( 'title' in _page && !( 'name' in _page ) ){ + _page.name = _page.title; + delete _page.title; + plugin.logger.warn( `Page ${pagePath()} is using deprecated "title" property. Use "name" instead.` ); + } + assert( 'name' in _page && isString( _page.name ), `Page ${pagePath()} should have a name` ); + const extraProps = difference( Object.keys( _page ), allowedKeys as string[] ); + assert.equal( extraProps.length, 0, `Page ${pagePath()} have extra properties ${JSON.stringify( extraProps )}` ); + if( 'children' in _page && !isNil( _page.children ) ){ + assert( isArray( _page.children ), `Page ${pagePath()} "children" should be an array` ); + const thisPath = [ ...path, _page.name as string ]; + _page.children.forEach( ( c, i ) => miscUtils.catchWrap( + () => checkPage( plugin, c, thisPath ), + wrapPageError( thisPath, i ) ) ); + } + } else { + throw new Error( `Page ${pagePath()} has a "match" or "template" property, but it should have both or none` ); } }; const checkPage = checkPageFactory( pageKeys ); @@ -64,11 +74,9 @@ export const buildOptions = ( plugin: PagesPlugin ) => OptionGroup.factory miscUtils.catchWrap( () => checkRootPage( plugin, p, [] ), wrapPageError( [], i ) ) ); - const rootFlags = groupBy( v, p => !!p.moduleRoot ); + const flattenRootNodes = ( vv: any ) => 'template' in vv ? vv.template.flatMap( ( vvv: any ) => flattenRootNodes( vvv ) ) : [ vv ]; + const rootFlags = groupBy( v.flatMap( vv => flattenRootNodes( vv ) ), p => !!p.moduleRoot ); assert.equal( Object.keys( rootFlags ).length, 1, 'Every root pages should set `moduleRoot` to true, or none' ); - if( rootFlags.true ) { - assert.equal( uniq( v.map( p => p.name ) ).length, v.length, 'Every root pages should have a different name' ); - } } }, }, v => { diff --git a/packages/plugin-pages/src/options/options.spec.ts b/packages/plugin-pages/src/options/options.spec.ts index b2554f4d..2bd45863 100644 --- a/packages/plugin-pages/src/options/options.spec.ts +++ b/packages/plugin-pages/src/options/options.spec.ts @@ -22,10 +22,6 @@ describe( 'Pages', () => { { name: 'A', moduleRoot: true }, { name: 'B' }, ] as IRootPageNode[] } ) ).toThrow( 'Every root pages should set `moduleRoot` to true, or none' ) ); - it( 'should throw if multiple "moduleRoot" are equal', () => expect( () => options.setValue( { pages: [ - { name: 'A', moduleRoot: true }, - { name: 'A', moduleRoot: true }, - ] as IRootPageNode[] } ) ).toThrow( 'Every root pages should have a different name' ) ); it( 'should warn if using legacy "title" property (#133)', () => { options.setValue( { pages: [ { title: 'A' }, @@ -33,4 +29,36 @@ describe( 'Pages', () => { expect( application.logger.warn ).toHaveBeenCalledTimes( 1 ); expect( application.logger.warn ).toHaveBeenCalledWith( expect.toInclude( 'Page "A" is using deprecated "title" property. Use "name" instead.' ) ); } ); + describe( 'Glob', () => { + it( 'should throw if page has a `match` property, but no `template`', () => { + expect( () => options.setValue( { pages: [ + { match: 'A' }, + ] } ) ).toThrow( 'Page "Unnamed" has a "match" or "template" property, but it should have both or none' ); + } ); + it( 'should throw if page has a `template` property, but no `match`', () => { + expect( () => options.setValue( { pages: [ + { template: [] }, + ] } ) ).toThrow( 'Page "Unnamed" has a "match" or "template" property, but it should have both or none' ); + } ); + it( 'should throw if page has a `template` property with invalid child', () => { + expect( () => options.setValue( { pages: [ + { match: 'foo', template: [ + { asd: false }, + ] }, + ] } ) ).toThrow( 'Page "TEMPLATE 1" ⇒ "Unnamed" should have a name' ); + } ); + it( 'should check for root nodes through templates', () => { + expect( () => options.setValue( { pages: [ + { match: 'foo', template: [ + { match: 'bar', template: [ + { match: 'qux', template: [ + { name: 'test' }, + ] }, + ] }, + ] }, + { moduleRoot: true, name: 'fail' }, + ] } ) ).toThrow( 'Every root pages should set `moduleRoot` to true, or none' ); + } ); + + } ); } ); From 4cba4d6d0a978d6c760006f34e987338a0c16d2a Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Thu, 28 Jul 2022 13:51:34 +0200 Subject: [PATCH 03/31] fix(plugin-pages): fix minor issue in child page input concatenation with `null` source --- .../page-tree/page-tree-builder.spec.ts | 21 ------------------- .../converter/page-tree/page-tree-builder.ts | 12 ++++++----- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts index 71260fb3..56f52bce 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.spec.ts @@ -242,27 +242,6 @@ describe( 'Simple tree', () => { ] } ), ] ); } ); - it( 'should map page to workspace with children with pages in module', () => { - addChildModule( 'SUB' ); - const targetModule = addChildModule( 'SUB2' ); - setVirtualFs( { - SUB2: { - 'appendix.md': 'APPENDIX', - 'bar.md': 'Bar content', - 'baz.md': 'Baz content', - }, - } ); - const out = pageTreeBuilder.buildPagesTree( project, opts( [ { name: 'SUB2', moduleRoot: true, source: 'appendix.md', children: [ - { name: 'Bar', source: 'bar.md' }, - { name: 'Baz', source: 'baz.md' }, - ] } ] ) ); - expect( out.childrenNodes ).toEqual( [ - matchReflection( PageReflection, { name: 'SUB2', depth: 0, module: targetModule, sourceFilePath: 'SUB2/appendix.md', childrenNodes: [ - matchReflection( PageReflection, { name: 'Bar', depth: 1, module: targetModule, sourceFilePath: 'SUB2/bar.md', content: 'Bar content', url: 'SUB2/bar.html' } ), - matchReflection( PageReflection, { name: 'Baz', depth: 1, module: targetModule, sourceFilePath: 'SUB2/baz.md', content: 'Baz content', url: 'SUB2/baz.html' } ), - ] } ), - ] ); - } ); } ); describe( 'Mixed', () => { it( 'should map appendixes to workspace and root', () => { diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts index 79b3e564..748bfaa9 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts @@ -170,7 +170,7 @@ export class PageTreeBuilder implements IPluginComponent { * @returns the node reflections. */ private _mapNodeToReflection( node: PageNode, parent: ANodeReflection.Parent, io: IIOPath ): NodeReflection[] { - const childrenIO: IIOPath = isModuleRoot( node ) ? { ...io } : { + const childrenIO: IIOPath = { ...io, input: join( io.input, getDir( node, 'source' ) ), output: join( io.output, getDir( node, 'output' ) ), @@ -181,11 +181,13 @@ export class PageTreeBuilder implements IPluginComponent { []; } const nodeReflection = this._getNodeReflection( node, parent, io ); - if( !( nodeReflection.module instanceof ProjectReflection ) && nodeReflection.isModuleAppendix ){ - // If the node is attached to a new module, skip changes in the input tree (stay at root of `pages` in module) + if( nodeReflection.isModuleAppendix ){ childrenIO.input = io.input; - // Output is now like `pkg-a/pages/...` - childrenIO.output = `${nodeReflection.name.replace( /[^a-z0-9]/gi, '_' )}/${io.output ?? ''}`; + childrenIO.output = io.output; + if( !( nodeReflection.module instanceof ProjectReflection ) ){ + // Output is now like `pkg-a/pages/...` + childrenIO.output = `${nodeReflection.name.replace( /[^a-z0-9]/gi, '_' )}/${io.output ?? ''}`; + } } const children = node.children ? this._mapNodesToReflectionsTree( From 5e046032d1ecc41b5ef1aed9c28f0ed90c9dd999 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Thu, 28 Jul 2022 13:57:12 +0200 Subject: [PATCH 04/31] chore(plugin-pages): deprecate the `source` option Related to #132 --- packages/plugin-pages/pages/options.md | 8 ++++--- .../converter/page-tree/page-tree-builder.ts | 22 +++++++++++-------- packages/plugin-pages/src/options/build.ts | 2 +- packages/plugin-pages/src/options/types.ts | 3 ++- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/plugin-pages/pages/options.md b/packages/plugin-pages/pages/options.md index d898c73e..7e8ab580 100644 --- a/packages/plugin-pages/pages/options.md +++ b/packages/plugin-pages/pages/options.md @@ -29,13 +29,15 @@ typedoc --help * `output`: Output directory where your pages will be rendered.\ Type: `string`\ Default: `'pages'` -* `source`: Root directory where all page source files live.\ - Type: `string`\ - Default: `'pages'` * `logLevel`: The plugin log level.\ Type: `LogLevel`\ Default to the application log level. * `excludeMarkdownTags`: A list of markdown captures to omit. Should have the form `{@....}`.\ Type: `string[]` +* `source`: **deprecated** Root directory where all page source files live.\ + Type: `string`\ + Default: `'pages'` + + > **Note**: prefer setting this option to `null` in order to anticipate a future removal of this option. > See {@link IPluginOptions} \ No newline at end of file diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts index 748bfaa9..cea7741e 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts @@ -18,9 +18,9 @@ const isModuleRoot = ( pageNode: IPageNode | IRootPageNode ) => 'moduleRoot' in type PageNode = IPageNode | IRootPageNode; interface IIOPath { - inputContainer?: string; - input?: string; - output?: string; + inputContainer: string | null; + input?: string | null; + output?: string | null; } export class PageTreeBuilder implements IPluginComponent { private readonly _logger = this.plugin.logger.makeChildLogger( PageTreeBuilder.name ); @@ -34,6 +34,9 @@ export class PageTreeBuilder implements IPluginComponent { * @returns the nodes tree. */ public buildPagesTree( project: ProjectReflection, options: IPluginOptions ): MenuReflection { + if( options.source ) { // TODO: Backward compatibility + this._logger.warn( 'Using deprecated option "source". We recommend setting it to `null` and use "VIRTUAL" menus with "childrenDir"' ); + } const rootMenu = new MenuReflection( 'ROOT', project, undefined, '' ); if( !options.pages || options.pages.length === 0 ){ return rootMenu; @@ -92,9 +95,9 @@ export class PageTreeBuilder implements IPluginComponent { * @param sourceDir - The pages container directory. * @returns the expanded page nodes. */ - private _expandRootPageNodes( pages: IPluginOptions.Page[], sourceDir: string ): IRootPageNode[]{ + private _expandRootPageNodes( pages: IPluginOptions.Page[], sourceDir?: string | null ): IRootPageNode[]{ const entryPoints = this.plugin.application.options.getValue( 'entryPoints' ).flatMap( ep => glob( ep ) ); - return pages.map( p => this._expandPageNode( entryPoints.map( ep => join( ep, sourceDir ) ), p, [] ) ).flat( 2 ); + return pages.map( p => this._expandPageNode( entryPoints, p, sourceDir, [] ) ).flat( 1 ); } /** @@ -102,12 +105,13 @@ export class PageTreeBuilder implements IPluginComponent { * * @param froms - A list of paths to try to expand against. * @param node - The node to expand. + * @param sourceDir - The container directory expected to contain all source pages. * @param prevContexts - A list of previous expansion contexts. * @returns the expanded nodes. */ - private _expandPageNode( froms: string[], node: OptionsPageNode | IOptionPatternPage, prevContexts: IExpandContext[] = [] ): T[] { + private _expandPageNode( froms: string[], node: OptionsPageNode | IOptionPatternPage, sourceDir?: string | null, prevContexts: IExpandContext[] = [] ): T[] { if( 'match' in node ){ - const matches = froms.flatMap( from => glob( node.match, { cwd: from } ).map( m => ( { + const matches = froms.flatMap( from => glob( node.match, { cwd: from.match( /^\.{1,2}\// ) ? from : join( from, sourceDir ) } ).map( m => ( { from, match: normalizePath( m ), fullPath: normalizePath( resolve( from, m ) ), @@ -116,7 +120,7 @@ export class PageTreeBuilder implements IPluginComponent { const nodesExpanded = matches.map( m => node.template.map( t => { const tClone = cloneDeep( t ); if( 'children' in tClone ){ - tClone.children = tClone.children?.map( c => this._expandPageNode( [ m.fullPath ], c, [ ...prevContexts, m ] ) ).flat( 1 ) ?? []; + tClone.children = tClone.children?.map( c => this._expandPageNode( [ m.fullPath ], c, sourceDir, [ ...prevContexts, m ] ) ).flat( 1 ) ?? []; } const expanded = expandNode( tClone, m ); return expanded; @@ -125,7 +129,7 @@ export class PageTreeBuilder implements IPluginComponent { } const clone = cloneDeep( node ); if( 'children' in clone ){ - clone.children = clone.children?.map( c => this._expandPageNode( froms, c, [ ...prevContexts ] ) ).flat( 1 ) ?? []; + clone.children = clone.children?.map( c => this._expandPageNode( froms, c, '.', [ ...prevContexts ] ) ).flat( 1 ) ?? []; } return [ clone as any ]; } diff --git a/packages/plugin-pages/src/options/build.ts b/packages/plugin-pages/src/options/build.ts index c97d86c9..481eb3c3 100644 --- a/packages/plugin-pages/src/options/build.ts +++ b/packages/plugin-pages/src/options/build.ts @@ -92,7 +92,7 @@ export const buildOptions = ( plugin: PagesPlugin ) => OptionGroup.factory v || null ) .add( 'logLevel', { help: 'The plugin log level.', type: ParameterType.Map, diff --git a/packages/plugin-pages/src/options/types.ts b/packages/plugin-pages/src/options/types.ts index c3942c92..00d352a5 100644 --- a/packages/plugin-pages/src/options/types.ts +++ b/packages/plugin-pages/src/options/types.ts @@ -124,9 +124,10 @@ export interface IPluginOptions { /** * Root directory where all page source files live. * + * @deprecated - Prefer setting this option to `null`. * @default 'pages' */ - source: string; + source: string | null; /** * The plugin log level. From e3b2f4f907ba763a8d379aa4707c3b6e2cc007de Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Thu, 28 Jul 2022 14:10:38 +0200 Subject: [PATCH 05/31] feat(plugin-pages): add `linkModuleBase` option --- packages/plugin-pages/pages/options.md | 4 +++- packages/plugin-pages/src/options/build.ts | 4 ++++ packages/plugin-pages/src/options/types.ts | 4 ++++ packages/plugin-pages/src/output/markdown-links.ts | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/plugin-pages/pages/options.md b/packages/plugin-pages/pages/options.md index 7e8ab580..77d10d16 100644 --- a/packages/plugin-pages/pages/options.md +++ b/packages/plugin-pages/pages/options.md @@ -34,10 +34,12 @@ typedoc --help Default to the application log level. * `excludeMarkdownTags`: A list of markdown captures to omit. Should have the form `{@....}`.\ Type: `string[]` +* `linkModuleBase`: The container in packages to search for pages in "{@link ...}" tags.\ + Type: `string | null` * `source`: **deprecated** Root directory where all page source files live.\ Type: `string`\ Default: `'pages'` - > **Note**: prefer setting this option to `null` in order to anticipate a future removal of this option. + > **Note**: prefer setting this option to `null` and use `linkModuleBase` in order to anticipate a future removal of this option. > See {@link IPluginOptions} \ No newline at end of file diff --git a/packages/plugin-pages/src/options/build.ts b/packages/plugin-pages/src/options/build.ts index 481eb3c3..871624f9 100644 --- a/packages/plugin-pages/src/options/build.ts +++ b/packages/plugin-pages/src/options/build.ts @@ -104,4 +104,8 @@ export const buildOptions = ( plugin: PagesPlugin ) => OptionGroup.factory patterns?.forEach( p => assert.match( p, /^\{@.*\}$/, `Pattern ${JSON.stringify( p )} should match "{@...}"` ) ), }, v => v ?? [] ) + .add( 'linkModuleBase', { + help: 'The container in packages to search for pages in "{@link ...}" tags.', + type: ParameterType.String, + }, v => v || null ) .build(); diff --git a/packages/plugin-pages/src/options/types.ts b/packages/plugin-pages/src/options/types.ts index 00d352a5..201f1230 100644 --- a/packages/plugin-pages/src/options/types.ts +++ b/packages/plugin-pages/src/options/types.ts @@ -140,6 +140,10 @@ export interface IPluginOptions { * A list of markdown captures to omit. Should have the form `{@....}`. */ excludeMarkdownTags?: string[]; + /** + * The container in packages to search for pages in "{@link ...}" tags. + */ + linkModuleBase: string | null; } export namespace IPluginOptions { export type Page = OptionsPageNode | IOptionPatternPage; diff --git a/packages/plugin-pages/src/output/markdown-links.ts b/packages/plugin-pages/src/output/markdown-links.ts index 9f79f653..c7732c8c 100644 --- a/packages/plugin-pages/src/output/markdown-links.ts +++ b/packages/plugin-pages/src/output/markdown-links.ts @@ -90,7 +90,7 @@ export class MarkdownPagesLinks implements IPluginComponent { assert( isString( pageAlias ) ); const resolvedFile = resolveNamedPath( this._currentPageMemo.currentReflection, - this.plugin.pluginOptions.getValue().source ?? undefined, + this.plugin.pluginOptions.getValue().linkModuleBase ?? this.plugin.pluginOptions.getValue().source ?? undefined, // TODO: Backward compatibility pageAlias as NamedPath ); const page = this._nodesReflections.find( m => m.sourceFilePath === resolvedFile ); assert( page, new Error( 'Page not found' ) ); From ca749568ae1c5cf0167aaf170443e3b0ec713447 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Wed, 3 Aug 2022 22:51:35 +0200 Subject: [PATCH 06/31] chore(plugin-pages): tweak module root declarations resolution --- .../src/converter/page-tree/page-tree-builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts index cea7741e..a1f9d17a 100644 --- a/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts +++ b/packages/plugin-pages/src/converter/page-tree/page-tree-builder.ts @@ -186,11 +186,11 @@ export class PageTreeBuilder implements IPluginComponent { } const nodeReflection = this._getNodeReflection( node, parent, io ); if( nodeReflection.isModuleAppendix ){ - childrenIO.input = io.input; - childrenIO.output = io.output; + childrenIO.input = node.childrenSourceDir ?? node.childrenDir ?? io.input; if( !( nodeReflection.module instanceof ProjectReflection ) ){ // Output is now like `pkg-a/pages/...` - childrenIO.output = `${nodeReflection.name.replace( /[^a-z0-9]/gi, '_' )}/${io.output ?? ''}`; + const output = node.childrenOutputDir ?? node.childrenDir ?? io.output; + childrenIO.output = `${nodeReflection.name.replace( /[^a-z0-9]/gi, '_' )}/${output ?? ''}`; } } const children = node.children ? From 143fae7d0e7fd8e3b7b620c1d15ccb98d1898260 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Thu, 4 Aug 2022 10:23:32 +0200 Subject: [PATCH 07/31] test(plugin-pages): update tests --- .../integration/__snapshots__/monorepo.spec.ts.snap | 11 +++++++++-- .../__tests__/integration/monorepo.spec.ts | 10 +++++++--- .../packages/a/{pages/readme-extras.md => README.md} | 2 +- .../mock-fs/monorepo/packages/a/src/index.ts | 2 +- .../__tests__/mock-fs/monorepo/packages/b/README.md | 3 +++ .../__tests__/mock-fs/monorepo/typedoc.js | 12 +++++++++--- 6 files changed, 30 insertions(+), 10 deletions(-) rename packages/plugin-pages/__tests__/mock-fs/monorepo/packages/a/{pages/readme-extras.md => README.md} (63%) create mode 100644 packages/plugin-pages/__tests__/mock-fs/monorepo/packages/b/README.md diff --git a/packages/plugin-pages/__tests__/integration/__snapshots__/monorepo.spec.ts.snap b/packages/plugin-pages/__tests__/integration/__snapshots__/monorepo.spec.ts.snap index ace9f17b..4448308f 100644 --- a/packages/plugin-pages/__tests__/integration/__snapshots__/monorepo.spec.ts.snap +++ b/packages/plugin-pages/__tests__/integration/__snapshots__/monorepo.spec.ts.snap @@ -442,8 +442,8 @@ exports[`pkg-a \`modules/pkg_a.html\` should have constant content 1`] = `

Module pkg-a

@@ -902,6 +902,13 @@ exports[`pkg-b \`modules/pkg_b.html\` should have constant content 1`] = `

Module pkg-b

+
+ +

README for B

+
+

See stubA, stubA, stubB or stubB

+
+