From 314f173d5430f452a9924569db8f38575337c638 Mon Sep 17 00:00:00 2001 From: GerkinDev Date: Thu, 3 Mar 2022 17:18:54 +0100 Subject: [PATCH] fix: various reflection path resolution fixes, better test code blocks --- .../plugin-code-blocks/__tests__/helpers.ts | 13 + .../__snapshots__/monorepo.spec.ts.snap | 523 ++++++++++++++++++ .../__snapshots__/simple.spec.ts.snap | 286 ++++++++++ .../__snapshots__/test.spec.ts.snap | 159 ------ .../__tests__/integration/monorepo.spec.ts | 42 ++ .../__tests__/integration/simple.spec.ts | 32 ++ .../__tests__/integration/test.spec.ts | 37 -- .../mock-fs/{ => monorepo}/README.md | 0 .../mock-fs/{ => monorepo}/blocks/test.json | 0 .../monorepo/packages/a/blocks/test.json | 1 + .../mock-fs/monorepo/packages/a/package.json | 4 + .../mock-fs/monorepo/packages/a/src/index.ts | 33 ++ .../{ => monorepo/packages/a}/tsconfig.json | 0 .../monorepo/packages/b/blocks/test.json | 1 + .../mock-fs/monorepo/packages/b/package.json | 4 + .../mock-fs/monorepo/packages/b/src/index.ts | 33 ++ .../mock-fs/monorepo/packages/b/tsconfig.json | 7 + .../__tests__/mock-fs/monorepo/tsconfig.json | 7 + .../__tests__/mock-fs/monorepo/typedoc.js | 7 + .../__tests__/mock-fs/simple/README.md | 0 .../__tests__/mock-fs/simple/blocks/test.json | 1 + .../__tests__/mock-fs/simple/src/test.json | 1 + .../__tests__/mock-fs/simple/src/test.ts | 40 ++ .../__tests__/mock-fs/simple/tsconfig.json | 7 + .../__tests__/mock-fs/simple/typedoc.js | 6 + .../__tests__/mock-fs/src/test.ts | 8 - .../src/code-sample-file.spec.ts | 4 +- .../src/code-sample-file.ts | 20 +- .../plugin-code-blocks/src/options/build.ts | 5 +- .../plugin-code-blocks/src/plugin.spec.ts | 15 +- packages/plugin-code-blocks/src/plugin.ts | 42 +- .../src/reflections/code-block-reflection.ts | 15 + .../src/reflections/index.ts | 2 + .../src/reflections/reflection-kind.ts | 13 + packages/plugin-pages/src/plugin.ts | 2 +- .../src/reflections/page-reflection.ts | 4 +- .../page-tree/a-page-tree-builder.ts | 8 +- packages/plugintestbed/src/index.ts | 1 + packages/plugintestbed/src/run-plugin.ts | 32 ++ packages/pluginutils/src/current-page-memo.ts | 32 +- .../pluginutils/src/markdown-replacer.spec.ts | 16 +- packages/pluginutils/src/markdown-replacer.ts | 8 +- .../src/path-reflection-resolver.ts | 58 +- packages/pluginutils/src/utils/misc.ts | 10 + 44 files changed, 1255 insertions(+), 284 deletions(-) create mode 100644 packages/plugin-code-blocks/__tests__/helpers.ts create mode 100644 packages/plugin-code-blocks/__tests__/integration/__snapshots__/monorepo.spec.ts.snap create mode 100644 packages/plugin-code-blocks/__tests__/integration/__snapshots__/simple.spec.ts.snap delete mode 100644 packages/plugin-code-blocks/__tests__/integration/__snapshots__/test.spec.ts.snap create mode 100644 packages/plugin-code-blocks/__tests__/integration/monorepo.spec.ts create mode 100644 packages/plugin-code-blocks/__tests__/integration/simple.spec.ts delete mode 100644 packages/plugin-code-blocks/__tests__/integration/test.spec.ts rename packages/plugin-code-blocks/__tests__/mock-fs/{ => monorepo}/README.md (100%) rename packages/plugin-code-blocks/__tests__/mock-fs/{ => monorepo}/blocks/test.json (100%) create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/blocks/test.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/package.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/src/index.ts rename packages/plugin-code-blocks/__tests__/mock-fs/{ => monorepo/packages/a}/tsconfig.json (100%) create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/blocks/test.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/package.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/src/index.ts create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/tsconfig.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/tsconfig.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/monorepo/typedoc.js create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/README.md create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/blocks/test.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.ts create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/tsconfig.json create mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/simple/typedoc.js delete mode 100644 packages/plugin-code-blocks/__tests__/mock-fs/src/test.ts create mode 100644 packages/plugin-code-blocks/src/reflections/code-block-reflection.ts create mode 100644 packages/plugin-code-blocks/src/reflections/index.ts create mode 100644 packages/plugin-code-blocks/src/reflections/reflection-kind.ts create mode 100644 packages/plugintestbed/src/run-plugin.ts diff --git a/packages/plugin-code-blocks/__tests__/helpers.ts b/packages/plugin-code-blocks/__tests__/helpers.ts new file mode 100644 index 00000000..a364e346 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/helpers.ts @@ -0,0 +1,13 @@ +import { JSDOM } from 'jsdom'; + +export const formatExpanded = ( file: string, content: string ) => `

From ${file}

${content}\n
`; +export const checkDef = ( dom: JSDOM, id: string, codeBlock: string ) => { + const link = dom.window.document.getElementById( id ); + expect( link ).toBeTruthy(); + const section = link!.parentElement; + expect( section ).toBeTruthy(); + const blocks = section!.querySelectorAll( '.code-block' ); + expect( blocks ).toHaveLength( 1 ); + const block = blocks[0]!; + expect( block.outerHTML ).toEqual( codeBlock ); +}; diff --git a/packages/plugin-code-blocks/__tests__/integration/__snapshots__/monorepo.spec.ts.snap b/packages/plugin-code-blocks/__tests__/integration/__snapshots__/monorepo.spec.ts.snap new file mode 100644 index 00000000..8d77042d --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/integration/__snapshots__/monorepo.spec.ts.snap @@ -0,0 +1,523 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Real behavior should render correctly 1`] = ` +" + + + + + + pkg-a | @knodes/typedoc-plugin-code-blocks + + + + + + + + + + +
+
+
+
+
+
+
    +
  • Preparing search index...
  • +
  • The search index is not available
  • +
@knodes/typedoc-plugin-code-blocks +
+
+
Options +
+
All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+
+
Menu +
+
+
+
+
+
+ +

Module pkg-a

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Functions

+
+

testInProjA + + + +

+
    +
  • testInProjA(): number
  • +
+
    +
  • + +
    +
    +

    A test code block targetting project A

    +
    +
    +
    + +

    From ./packages/a/blocks/test.json

    +
    +
    {"pkg": "a"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testInProjB + + + +

+
    +
  • testInProjB(): number
  • +
+
    +
  • + +
    +
    +

    A test code block targetting project B

    +
    +
    +
    + +

    From ./packages/b/blocks/test.json

    +
    +
    {"pkg": "b"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testNoPrefixImplicitInBlocks + + + +

+
    +
  • testNoPrefixImplicitInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path implicitly in blocks directory

    +
    +
    +
    + +

    From ./packages/a/blocks/test.json

    +
    +
    {"pkg": "a"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testNoPrefixInBlocks + + + +

+
    +
  • testNoPrefixInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path in blocks directory

    +
    +
    +
    + +

    From ./packages/a/blocks/test.json

    +
    +
    {"pkg": "a"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + +" +`; + +exports[`Real behavior should render correctly 2`] = ` +" + + + + + + pkg-b | @knodes/typedoc-plugin-code-blocks + + + + + + + + + + +
+
+
+
+
+
+
    +
  • Preparing search index...
  • +
  • The search index is not available
  • +
@knodes/typedoc-plugin-code-blocks +
+
+
Options +
+
All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+
+
Menu +
+
+
+
+
+
+ +

Module pkg-b

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Functions

+
+

testInProjA + + + +

+
    +
  • testInProjA(): number
  • +
+
    +
  • + +
    +
    +

    A test code block targetting project A

    +
    +
    +
    + +

    From ./packages/a/blocks/test.json

    +
    +
    {"pkg": "a"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testInProjB + + + +

+
    +
  • testInProjB(): number
  • +
+
    +
  • + +
    +
    +

    A test code block targetting project B

    +
    +
    +
    + +

    From ./packages/b/blocks/test.json

    +
    +
    {"pkg": "b"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testNoPrefixImplicitInBlocks + + + +

+
    +
  • testNoPrefixImplicitInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path implicitly in blocks directory

    +
    +
    +
    + +

    From ./packages/b/blocks/test.json

    +
    +
    {"pkg": "b"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testNoPrefixInBlocks + + + +

+
    +
  • testNoPrefixInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path in blocks directory

    +
    +
    +
    + +

    From ./packages/b/blocks/test.json

    +
    +
    {"pkg": "b"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + +" +`; diff --git a/packages/plugin-code-blocks/__tests__/integration/__snapshots__/simple.spec.ts.snap b/packages/plugin-code-blocks/__tests__/integration/__snapshots__/simple.spec.ts.snap new file mode 100644 index 00000000..52a55a02 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/integration/__snapshots__/simple.spec.ts.snap @@ -0,0 +1,286 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Real behavior should render correctly 1`] = ` +" + + + + + + @knodes/typedoc-plugin-code-blocks + + + + + + + + + + +
+
+
+
+
+
+
    +
  • Preparing search index...
  • +
  • The search index is not available
  • +
@knodes/typedoc-plugin-code-blocks +
+
+
Options +
+
All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+
+
Menu +
+
+
+
+
+
+

@knodes/typedoc-plugin-code-blocks

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Functions

+
+

testNoPrefixImplicitInBlocks + + + +

+
    +
  • testNoPrefixImplicitInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path implicitly in blocks directory

    +
    +
    +
    + +

    From ./blocks/test.json

    +
    +
    {"Hello": "World"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testNoPrefixInBlocks + + + +

+
    +
  • testNoPrefixInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for unprefixed path in blocks directory

    +
    +
    +
    + +

    From ./blocks/test.json

    +
    +
    {"Hello": "World"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testProjImplicitInBlocks + + + +

+
    +
  • testProjImplicitInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for project path implicitly in blocks directory

    +
    +
    +
    + +

    From ./blocks/test.json

    +
    +
    {"Hello": "World"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testProjInBlocks + + + +

+
    +
  • testProjInBlocks(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for project path in blocks directory

    +
    +
    +
    + +

    From ./blocks/test.json

    +
    +
    {"Hello": "World"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+

testRel + + + +

+
    +
  • testRel(): number
  • +
+
    +
  • + +
    +
    +

    A test code block for relative path from file

    +
    +
    +
    + +

    From ./src/test.json

    +
    +
    {"Foo": "Bar"}
    +
    +
    +
    +
    +

    Returns number

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + +" +`; diff --git a/packages/plugin-code-blocks/__tests__/integration/__snapshots__/test.spec.ts.snap b/packages/plugin-code-blocks/__tests__/integration/__snapshots__/test.spec.ts.snap deleted file mode 100644 index 06188fbb..00000000 --- a/packages/plugin-code-blocks/__tests__/integration/__snapshots__/test.spec.ts.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Real behavior should render correctly 1`] = ` -" - - - - - - Test | @knodes/typedoc-plugin-code-blocks - - - - - - - - - - -
-
-
-
-
-
-
    -
  • Preparing search index...
  • -
  • The search index is not available
  • -
@knodes/typedoc-plugin-code-blocks -
-
-
Options -
-
All -
    -
  • Public
  • -
  • Public/Protected
  • -
  • All
  • -
-
-
-
Menu -
-
-
-
-
-
- -

Class Test

-
-
-
-
-
-
-
-
-
-

A test class

-
-
-
- -

From ./blocks/test.json

-
-
{"Hello": "World"}
-
-
-
-
-
-
-

Hierarchy

-
    -
  • Test
  • -
-
-
-

Index

-
-
-
-

Constructors

- -
-
-
-
-
-

Constructors

-
-

constructor - - - -

-
    -
  • new Test(): Test
  • -
-
    -
  • -

    Returns Test

    -
  • -
-
-
-
-
- - -
-
-
- -
-

Generated using TypeDoc

-
-
- - - -" -`; diff --git a/packages/plugin-code-blocks/__tests__/integration/monorepo.spec.ts b/packages/plugin-code-blocks/__tests__/integration/monorepo.spec.ts new file mode 100644 index 00000000..75ab4f8b --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/integration/monorepo.spec.ts @@ -0,0 +1,42 @@ +import { readFile } from 'fs/promises'; +import { resolve } from 'path'; + +import { JSDOM } from 'jsdom'; + +import { formatHtml, runPlugin } from '@knodes/typedoc-plugintestbed'; + +import { checkDef, formatExpanded } from '../helpers'; + +const rootDir = resolve( __dirname, '../mock-fs/monorepo' ); +jest.setTimeout( 30000 ); +beforeEach( () => { + process.chdir( rootDir ); +} ); +describe( 'Real behavior', () => { + it( 'should render correctly', async () => { + await runPlugin( rootDir, resolve( __dirname, '../../src/index' ) ); + const testJson = '
'+
+		'{"Hello": "World"}\n'+
+		'
'; + const testJsonA = testJson.replace( 'Hello', 'pkg' ).replace( 'World', 'a' ); + const testJsonB = testJson.replace( 'Hello', 'pkg' ).replace( 'World', 'b' ); + + const pkgA = await readFile( resolve( rootDir, 'docs/modules/pkg_a.html' ), 'utf-8' ); + const domA = new JSDOM( pkgA ); + checkDef( domA, 'testInProjA', formatExpanded( './packages/a/blocks/test.json', testJsonA ) ); + checkDef( domA, 'testInProjB', formatExpanded( './packages/b/blocks/test.json', testJsonB ) ); + checkDef( domA, 'testNoPrefixImplicitInBlocks', formatExpanded( './packages/a/blocks/test.json', testJsonA ) ); + checkDef( domA, 'testNoPrefixInBlocks', formatExpanded( './packages/a/blocks/test.json', testJsonA ) ); + expect( pkgA ).toMatch( // ); + expect( formatHtml( pkgA ) ).toMatchSnapshot(); + + const pkgB = await readFile( resolve( rootDir, 'docs/modules/pkg_b.html' ), 'utf-8' ); + const domB = new JSDOM( pkgB ); + checkDef( domB, 'testInProjA', formatExpanded( './packages/a/blocks/test.json', testJsonA ) ); + checkDef( domB, 'testInProjB', formatExpanded( './packages/b/blocks/test.json', testJsonB ) ); + checkDef( domB, 'testNoPrefixImplicitInBlocks', formatExpanded( './packages/b/blocks/test.json', testJsonB ) ); + checkDef( domB, 'testNoPrefixInBlocks', formatExpanded( './packages/b/blocks/test.json', testJsonB ) ); + expect( pkgB ).toMatch( // ); + expect( formatHtml( pkgB ) ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/plugin-code-blocks/__tests__/integration/simple.spec.ts b/packages/plugin-code-blocks/__tests__/integration/simple.spec.ts new file mode 100644 index 00000000..a7edfb73 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/integration/simple.spec.ts @@ -0,0 +1,32 @@ +import { readFile } from 'fs/promises'; +import { resolve } from 'path'; + +import { JSDOM } from 'jsdom'; + +import { formatHtml, runPlugin } from '@knodes/typedoc-plugintestbed'; + +import { checkDef, formatExpanded } from '../helpers'; + +const rootDir = resolve( __dirname, '../mock-fs/simple' ); +jest.setTimeout( 30000 ); +beforeEach( () => { + process.chdir( rootDir ); +} ); +describe( 'Real behavior', () => { + it( 'should render correctly', async () => { + await runPlugin( rootDir, resolve( __dirname, '../../src/index' ) ); + const c = await readFile( resolve( rootDir, 'docs/modules.html' ), 'utf-8' ); + const dom = new JSDOM( c ); + const testJson = '
'+
+		'{"Hello": "World"}\n'+
+		'
'; + const testJsonFooBar = testJson.replace( 'Hello', 'Foo' ).replace( 'World', 'Bar' ); + checkDef( dom, 'testProjImplicitInBlocks', formatExpanded( './blocks/test.json', testJson ) ); + checkDef( dom, 'testProjInBlocks', formatExpanded( './blocks/test.json', testJson ) ); + checkDef( dom, 'testNoPrefixImplicitInBlocks', formatExpanded( './blocks/test.json', testJson ) ); + checkDef( dom, 'testNoPrefixInBlocks', formatExpanded( './blocks/test.json', testJson ) ); + checkDef( dom, 'testRel', formatExpanded( './src/test.json', testJsonFooBar ) ); + expect( c ).toMatch( // ); + expect( formatHtml( c ) ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/plugin-code-blocks/__tests__/integration/test.spec.ts b/packages/plugin-code-blocks/__tests__/integration/test.spec.ts deleted file mode 100644 index 3d1a488b..00000000 --- a/packages/plugin-code-blocks/__tests__/integration/test.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { readFile } from 'fs/promises'; -import { resolve } from 'path'; - -import { Application, ArgumentsReader, TSConfigReader, TypeDocOptions, TypeDocReader } from 'typedoc'; - -import { formatHtml } from '@knodes/typedoc-plugintestbed'; - -const rootDir = resolve( __dirname, '../mock-fs' ); -jest.setTimeout( 30000 ); -beforeEach( () => { - process.chdir( rootDir ); -} ); -describe( 'Real behavior', () => { - it( 'should render correctly', async () => { - const app = new Application(); - app.options.addReader( new ArgumentsReader( 0, [] ) ); - app.options.addReader( new TypeDocReader() ); - app.options.addReader( new TSConfigReader() ); - const baseOptions: Partial = { - entryPoints: [ resolve( rootDir, './src/test.ts' ) ], - tsconfig: resolve( rootDir, './tsconfig.json' ), - treatWarningsAsErrors: true, - plugin: [ resolve( __dirname, '../../src/index' ) ], - }; - app.bootstrap( { - ...baseOptions, - 'pluginCodeBlocks:source': 'blocks', - } as any ); - const project = app.convert()!; - app.validate( project ); - await app.generateDocs( project, resolve( rootDir, './docs' ) ); - const c = await readFile( resolve( rootDir, 'docs/classes/Test.html' ), 'utf-8' ); - expect( c ).toContain( '{"Hello": "World"}' ); - expect( c ).toMatch( // ); - expect( formatHtml( c ) ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/README.md b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/README.md similarity index 100% rename from packages/plugin-code-blocks/__tests__/mock-fs/README.md rename to packages/plugin-code-blocks/__tests__/mock-fs/monorepo/README.md diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/blocks/test.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/blocks/test.json similarity index 100% rename from packages/plugin-code-blocks/__tests__/mock-fs/blocks/test.json rename to packages/plugin-code-blocks/__tests__/mock-fs/monorepo/blocks/test.json diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/blocks/test.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/blocks/test.json new file mode 100644 index 00000000..61491d21 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/blocks/test.json @@ -0,0 +1 @@ +{"pkg": "a"} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/package.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/package.json new file mode 100644 index 00000000..0e358379 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/package.json @@ -0,0 +1,4 @@ +{ + "name": "pkg-a", + "typedocMain": "./src" +} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/src/index.ts b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/src/index.ts new file mode 100644 index 00000000..757929eb --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/src/index.ts @@ -0,0 +1,33 @@ +const stub = () => 1; + +// #region inExplicitPackage +/** + * A test code block targetting project A + * + * {@codeblock ~pkg-a/test.json} + */ +export const testInProjA = stub; + +/** + * A test code block targetting project B + * + * {@codeblock ~pkg-b/test.json} + */ +export const testInProjB = stub; +// #endregion + +// #region inPackage +/** + * A test code block for unprefixed path implicitly in blocks directory + * + * {@codeblock test.json} + */ +export const testNoPrefixImplicitInBlocks = stub; + +/** + * A test code block for unprefixed path in blocks directory + * + * {@codeblock blocks/test.json} + */ +export const testNoPrefixInBlocks = stub; +// #endregion diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/tsconfig.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/tsconfig.json similarity index 100% rename from packages/plugin-code-blocks/__tests__/mock-fs/tsconfig.json rename to packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/a/tsconfig.json diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/blocks/test.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/blocks/test.json new file mode 100644 index 00000000..b8ffa262 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/blocks/test.json @@ -0,0 +1 @@ +{"pkg": "b"} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/package.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/package.json new file mode 100644 index 00000000..2bb1c328 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/package.json @@ -0,0 +1,4 @@ +{ + "name": "pkg-b", + "typedocMain": "./src" +} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/src/index.ts b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/src/index.ts new file mode 100644 index 00000000..757929eb --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/src/index.ts @@ -0,0 +1,33 @@ +const stub = () => 1; + +// #region inExplicitPackage +/** + * A test code block targetting project A + * + * {@codeblock ~pkg-a/test.json} + */ +export const testInProjA = stub; + +/** + * A test code block targetting project B + * + * {@codeblock ~pkg-b/test.json} + */ +export const testInProjB = stub; +// #endregion + +// #region inPackage +/** + * A test code block for unprefixed path implicitly in blocks directory + * + * {@codeblock test.json} + */ +export const testNoPrefixImplicitInBlocks = stub; + +/** + * A test code block for unprefixed path in blocks directory + * + * {@codeblock blocks/test.json} + */ +export const testNoPrefixInBlocks = stub; +// #endregion diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/tsconfig.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/tsconfig.json new file mode 100644 index 00000000..2436591d --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/packages/b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS" + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/tsconfig.json b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/tsconfig.json new file mode 100644 index 00000000..2436591d --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS" + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/typedoc.js b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/typedoc.js new file mode 100644 index 00000000..a0edeb08 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/monorepo/typedoc.js @@ -0,0 +1,7 @@ +module.exports = { + 'entryPoints': [ + 'packages/*', + ], + 'entryPointStrategy': 'packages', + 'pluginCodeBlocks:source': 'blocks', +}; diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/README.md b/packages/plugin-code-blocks/__tests__/mock-fs/simple/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/blocks/test.json b/packages/plugin-code-blocks/__tests__/mock-fs/simple/blocks/test.json new file mode 100644 index 00000000..2e57bd1e --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/simple/blocks/test.json @@ -0,0 +1 @@ +{"Hello": "World"} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.json b/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.json new file mode 100644 index 00000000..3228b6a8 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.json @@ -0,0 +1 @@ +{"Foo": "Bar"} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.ts b/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.ts new file mode 100644 index 00000000..cca8a907 --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/simple/src/test.ts @@ -0,0 +1,40 @@ +const stub = () => 1; + +// #region relPath +/** + * A test code block for relative path from file + * + * {@codeblock ./test.json} + */ +export const testRel = stub; +// #endregion + +// #region projPath +/** + * A test code block for unprefixed path implicitly in blocks directory + * + * {@codeblock test.json} + */ +export const testNoPrefixImplicitInBlocks = stub; + +/** + * A test code block for unprefixed path in blocks directory + * + * {@codeblock blocks/test.json} + */ +export const testNoPrefixInBlocks = stub; + +/** + * A test code block for project path in blocks directory + * + * {@codeblock ~~/blocks/test.json} + */ +export const testProjInBlocks = stub; + +/** + * A test code block for project path implicitly in blocks directory + * + * {@codeblock ~~/test.json} + */ +export const testProjImplicitInBlocks = stub; +// #endregion diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/tsconfig.json b/packages/plugin-code-blocks/__tests__/mock-fs/simple/tsconfig.json new file mode 100644 index 00000000..2436591d --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/simple/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS" + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/simple/typedoc.js b/packages/plugin-code-blocks/__tests__/mock-fs/simple/typedoc.js new file mode 100644 index 00000000..3e2629da --- /dev/null +++ b/packages/plugin-code-blocks/__tests__/mock-fs/simple/typedoc.js @@ -0,0 +1,6 @@ +module.exports = { + 'entryPoints': [ + './src/test.ts', + ], + 'pluginCodeBlocks:source': 'blocks', +}; diff --git a/packages/plugin-code-blocks/__tests__/mock-fs/src/test.ts b/packages/plugin-code-blocks/__tests__/mock-fs/src/test.ts deleted file mode 100644 index 9307649f..00000000 --- a/packages/plugin-code-blocks/__tests__/mock-fs/src/test.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * A test class - * - * {@codeblock blocks/test.json} - */ -export class Test { - -} diff --git a/packages/plugin-code-blocks/src/code-sample-file.spec.ts b/packages/plugin-code-blocks/src/code-sample-file.spec.ts index 6f463035..23edb436 100644 --- a/packages/plugin-code-blocks/src/code-sample-file.spec.ts +++ b/packages/plugin-code-blocks/src/code-sample-file.spec.ts @@ -91,6 +91,8 @@ Baz 'test.txt': content, } ); const res = Object.fromEntries( readCodeSample( 'test.txt' ).entries() ); - expect( res ).toEqual( output ); + const expected = Object.fromEntries( Object.entries( output ) + .map( ( [ k, v ] ) => [ k, { ...v, file: 'test.txt', region: k } ] ) ); + expect( res ).toEqual( expected ); } ); } ); diff --git a/packages/plugin-code-blocks/src/code-sample-file.ts b/packages/plugin-code-blocks/src/code-sample-file.ts index 7acb0bac..a724f805 100644 --- a/packages/plugin-code-blocks/src/code-sample-file.ts +++ b/packages/plugin-code-blocks/src/code-sample-file.ts @@ -1,9 +1,13 @@ import { readFileSync } from 'fs'; +import { textUtils } from '@knodes/typedoc-pluginutils'; + export const DEFAULT_BLOCK_NAME = '__DEFAULT__'; const REGION_REGEX = /^[\t ]*\/\/[\t ]*#((?:end)?region)(?:[\t ]+(.*?))?$/gm; export interface ICodeSample { + region: string; + file: string; code: string; startLine: number; endLine: number; @@ -32,6 +36,8 @@ export const readCodeSample = ( file: string ): Map => { if( regionMarkers.length === 0 ){ return new Map( [ [ DEFAULT_BLOCK_NAME, { + file, + region: DEFAULT_BLOCK_NAME, code: content, endLine: lines.length, startLine: 1, @@ -49,7 +55,8 @@ export const readCodeSample = ( file: string ): Map => { if( type === 'start' && !name ){ throw new Error( 'Missing name of #region' ); } - return { type, name, fullMatch: m[0], line: getLineNumberofChar( lines, m.index ) }; + const location = textUtils.getCoordinates( content, m.index ); + return { ...location, type, name, fullMatch: m[0] }; } ) .reduce( ( acc, marker ) => { if( marker.type === 'start' ){ @@ -93,6 +100,8 @@ export const readCodeSample = ( file: string ): Map => { .reduce( ( acc, { open, close, name } ) => { const code = lines.slice( open.line, close.line ).filter( l => !l.match( REGION_REGEX ) ).join( '\n' ); acc.set( name, { + file, + region: name, endLine: close.line, startLine: open.line, code, @@ -100,12 +109,3 @@ export const readCodeSample = ( file: string ): Map => { return acc; }, new Map() ); }; -const getLineNumberofChar = ( perLine: string[], index: number ): number => { - let totalLength = 0; - for ( let i = 0; i < perLine.length; i++ ) { - totalLength += perLine[i].length; - if ( totalLength >= index ) - return i + 1; - } - throw new Error(); -}; diff --git a/packages/plugin-code-blocks/src/options/build.ts b/packages/plugin-code-blocks/src/options/build.ts index 8c2055fe..a05f41dd 100644 --- a/packages/plugin-code-blocks/src/options/build.ts +++ b/packages/plugin-code-blocks/src/options/build.ts @@ -1,4 +1,4 @@ -import { LogLevel, ParameterHint, ParameterType } from 'typedoc'; +import { LogLevel, ParameterType } from 'typedoc'; import { OptionGroup } from '@knodes/typedoc-pluginutils'; @@ -21,8 +21,7 @@ export const buildOptions = ( plugin: CodeBlockPlugin ) => OptionGroup.factory { }, } ); } ); - it.only( 'should not affect text if no code block', () => { + it( 'should not affect text if no code block', () => { const text = 'Hello world' ; expect( markdownReplacerTestbed.runMarkdownReplace( text ) ).toEqual( text ); } ); @@ -68,6 +68,7 @@ describe( 'Behavior', () => { blocks: Array<[string, {code: string;startLine: number;endLine: number}]>; withGitHub: boolean; } + const defaultBlock = { startLine: 1, endLine: 1, file, region: DEFAULT_BLOCK_NAME as string }; const helloRegion: IBlockGenerationAssertion['blocks'] = [[ 'hello', { code: 'Content of foo/qux.txt', startLine: 13, endLine: 24 } ]]; it.each<[label: string, source: string, assertion: Partial]>( [ [ 'Mode ⇒ code block', `{@codeblock ${file} default}`, { renderCall: { mode: EBlockMode.DEFAULT }} ], @@ -85,7 +86,7 @@ describe( 'Behavior', () => { if( withGitHub ){ application.converter.addComponent( 'git-hub', FakeGitHub as any ); } - readCodeSampleMock.mockReturnValue( new Map( blocks ?? [[ DEFAULT_BLOCK_NAME, { code, startLine: 1, endLine: 1 } ]] ) ); + readCodeSampleMock.mockReturnValue( new Map( blocks?.map( ( [ name, b ] ) => [ name, { ...b, file, region: name } ] as const ) ?? [[ DEFAULT_BLOCK_NAME, { code, ...defaultBlock } ]] ) ); const { elemStr, renderCodeBlock } = setup( uuid => JSX.createElement( 'p', {}, uuid ) ); const callOut = markdownReplacerTestbed.runMarkdownReplace( source ); expect( getCodeBlockRendererMock ).toHaveBeenCalledTimes( 1 ); @@ -102,13 +103,13 @@ describe( 'Behavior', () => { it( 'should throw if region does not exists', () => { setVirtualFs( { foo: { 'bar.txt': '' }} ); setup( uuid => JSX.createElement( 'p', {}, uuid ) ); - readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code, startLine: 1, endLine: 1 } ]] ) ); - expect( () => markdownReplacerTestbed.runMarkdownReplace( '{@codeblock foo/bar.txt#nope}' ) ).toThrowWithMessage( Error, /^Missing block nope/ ); + readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code, ...defaultBlock } ]] ) ); + expect( () => markdownReplacerTestbed.runMarkdownReplace( '{@codeblock foo/bar.txt#nope}' ) ).toThrowWithMessage( Error, /^Missing block nope/m ); } ); it( 'should throw if invalid mode', () => { setup( uuid => JSX.createElement( 'p', {}, uuid ) ); - readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code, startLine: 1, endLine: 1 } ]] ) ); - expect( () => markdownReplacerTestbed.runMarkdownReplace( '{@codeblock foo/bar.txt asdasd}' ) ).toThrowWithMessage( AssertionError, /^Invalid block mode "asdasd"/ ); + readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code, ...defaultBlock } ]] ) ); + expect( () => markdownReplacerTestbed.runMarkdownReplace( '{@codeblock foo/bar.txt asdasd}' ) ).toThrowWithMessage( Error, /^Invalid block mode "asdasd"/m ); } ); } ); } ); diff --git a/packages/plugin-code-blocks/src/plugin.ts b/packages/plugin-code-blocks/src/plugin.ts index 353799be..9f5f034a 100644 --- a/packages/plugin-code-blocks/src/plugin.ts +++ b/packages/plugin-code-blocks/src/plugin.ts @@ -2,13 +2,14 @@ import assert from 'assert'; import { relative } from 'path'; import { once } from 'lodash'; -import { Application, JSX, LogLevel } from 'typedoc'; +import { Application, JSX, LogLevel, PageEvent, Reflection } from 'typedoc'; 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'; import { buildOptions } from './options'; +import { CodeBlockReflection } from './reflections'; import { EBlockMode } from './types'; const EXTRACT_CODE_BLOCKS_REGEX = /\{@codeblock\s+(\S+?\w+?)(?:#(.+?))?(?:\s+(\w+))?(?:\s*\|\s*(.*?))?\}/g; @@ -18,7 +19,7 @@ const EXTRACT_CODE_BLOCKS_REGEX = /\{@codeblock\s+(\S+?\w+?)(?:#(.+?))?(?:\s+(\w export class CodeBlockPlugin extends ABasePlugin { public readonly pluginOptions = buildOptions( this ); private readonly _codeBlockRenderer = once( () => getCodeBlockRenderer( this.application, this ) ); - private readonly _currentPageMemo = new CurrentPageMemo( this ); + private readonly _currentPageMemo = CurrentPageMemo.for( this ); private readonly _markdownReplacer = new MarkdownReplacer( this ); private readonly _pathReflectionResolver = new PathReflectionResolver( this ); private readonly _fileSamples = new Map>(); @@ -55,17 +56,22 @@ export class CodeBlockPlugin extends ABasePlugin { { captures, fullMatch }: Parameters[0], sourceHint: Parameters[1], ): ReturnType { + // Avoid recursion in code blocks + if( this._currentPageMemo.currentReflection instanceof CodeBlockReflection ){ + return fullMatch; + } + // Support escaped tags if( fullMatch.startsWith( '{\\@' ) ){ this.logger.verbose( () => `Found an escaped tag "${fullMatch}" in "${sourceHint()}"` ); return fullMatch.replace( '{\\@', '{@' ); } + // Extract informations const [ file, block, blockModeStr, fakedFileName ] = captures; const blockMode = blockModeStr ? EBlockMode[blockModeStr.toUpperCase() as keyof typeof EBlockMode] ?? assert.fail( `Invalid block mode "${blockModeStr}".` ) : this.pluginOptions.getValue().defaultBlockMode ?? EBlockMode.EXPANDED; assert.ok( file ); - // Use ??= once on node>14 - const defaultedBlock = block ?? DEFAULT_BLOCK_NAME; + const defaultedBlock = block ?? DEFAULT_BLOCK_NAME; // TODO: Use ??= once on node>14 const useWholeFile = defaultedBlock === DEFAULT_BLOCK_NAME; const resolvedFile = this._pathReflectionResolver.resolveNamedPath( this._currentPageMemo.currentReflection.project, @@ -80,6 +86,7 @@ export class CodeBlockPlugin extends ABasePlugin { } else { this.logger.verbose( () => `Created a code block to ${this.relativeToRoot( resolvedFile )} from "${sourceHint()}"` ); } + // Get the actual code sample if( !this._fileSamples.has( resolvedFile ) ){ this._fileSamples.set( resolvedFile, readCodeSample( resolvedFile ) ); } @@ -90,20 +97,25 @@ export class CodeBlockPlugin extends ABasePlugin { throw new Error( `Missing block ${defaultedBlock} in ${resolvedFile}` ); } + // Render const headerFileName = fakedFileName ?? `./${relative( this.rootDir, resolvedFile )}${useWholeFile ? '' : `#${codeSample.startLine}~${codeSample.endLine}`}`; const url = this._resolveCodeSampleUrl( resolvedFile, useWholeFile ? null : codeSample ); - const rendered = this._codeBlockRenderer().renderCodeBlock( { - asFile: headerFileName, - content: codeSample.code, - mode: blockMode, - sourceFile: resolvedFile, - url, + const fakePage = new PageEvent( codeSample.file ); + fakePage.model = new CodeBlockReflection( codeSample.region, codeSample.file, codeSample.code, codeSample.startLine, codeSample.endLine ); + return this._currentPageMemo.fakeWrapPage( fakePage, () => { + const rendered = this._codeBlockRenderer().renderCodeBlock( { + asFile: headerFileName, + content: codeSample.code, + mode: blockMode, + sourceFile: resolvedFile, + url, + } ); + if( typeof rendered === 'string' ){ + return rendered; + } else { + return JSX.renderElement( rendered ); + } } ); - if( typeof rendered === 'string' ){ - return rendered; - } else { - return JSX.renderElement( rendered ); - } } /** diff --git a/packages/plugin-code-blocks/src/reflections/code-block-reflection.ts b/packages/plugin-code-blocks/src/reflections/code-block-reflection.ts new file mode 100644 index 00000000..76739efd --- /dev/null +++ b/packages/plugin-code-blocks/src/reflections/code-block-reflection.ts @@ -0,0 +1,15 @@ +import { DeclarationReflection } from 'typedoc'; + +import { ECodeBlockReflectionKind } from './reflection-kind'; + +export class CodeBlockReflection extends DeclarationReflection { + public constructor( + name: string, + private readonly file: string, + private readonly code: string, + private readonly startLine: number, + private readonly endLine: number, + ){ + super( name, ECodeBlockReflectionKind.CODE_BLOCK as any ); + } +} diff --git a/packages/plugin-code-blocks/src/reflections/index.ts b/packages/plugin-code-blocks/src/reflections/index.ts new file mode 100644 index 00000000..a3ff786a --- /dev/null +++ b/packages/plugin-code-blocks/src/reflections/index.ts @@ -0,0 +1,2 @@ +export * from './code-block-reflection'; +export * from './reflection-kind'; diff --git a/packages/plugin-code-blocks/src/reflections/reflection-kind.ts b/packages/plugin-code-blocks/src/reflections/reflection-kind.ts new file mode 100644 index 00000000..03897638 --- /dev/null +++ b/packages/plugin-code-blocks/src/reflections/reflection-kind.ts @@ -0,0 +1,13 @@ +import { ReflectionKind } from 'typedoc'; + +import { addReflectionKind } from '@knodes/typedoc-pluginutils'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires -- Get name from package +const ns = require( '../../package.json' ).name; +/** + * Extends the {@link ReflectionKind} to add custom Page, Menu & Any kinds. + */ +export enum ECodeBlockReflectionKind { + CODE_BLOCK = addReflectionKind( ns, 'CodeBlock' ), +} +( ECodeBlockReflectionKind as unknown as ReflectionKind ); diff --git a/packages/plugin-pages/src/plugin.ts b/packages/plugin-pages/src/plugin.ts index a85558fc..608c8dd0 100644 --- a/packages/plugin-pages/src/plugin.ts +++ b/packages/plugin-pages/src/plugin.ts @@ -13,7 +13,7 @@ const EXTRACT_PAGE_LINK_REGEX = /{\\?@page\s+([^}\s]+)(?:\s+([^}]+?))?\s*}/g; export class PagesPlugin extends ABasePlugin { public readonly pluginOptions = buildOptions( this ); private readonly _pageTreeBuilder = once( () => initThemePlugins( this.application, this ) ); - private readonly _currentPageMemo = new CurrentPageMemo( this ); + private readonly _currentPageMemo = CurrentPageMemo.for( this ); private readonly _markdownReplacer = new MarkdownReplacer( this ); private readonly _pathReflectionResolver = new PathReflectionResolver( this ); public constructor( application: Application ){ diff --git a/packages/plugin-pages/src/reflections/page-reflection.ts b/packages/plugin-pages/src/reflections/page-reflection.ts index a0ceff90..6ce9831b 100644 --- a/packages/plugin-pages/src/reflections/page-reflection.ts +++ b/packages/plugin-pages/src/reflections/page-reflection.ts @@ -3,7 +3,7 @@ import { relative } from 'path'; import { Comment, DeclarationReflection, ProjectReflection, ReflectionKind, SourceFile } from 'typedoc'; -import { rethrow } from '@knodes/typedoc-pluginutils'; +import { rethrow, wrapError } from '@knodes/typedoc-pluginutils'; import { ANodeReflection } from './a-node-reflection'; @@ -20,7 +20,7 @@ export class PageReflection extends ANodeReflection { super( name, kind, module, parent ); this.content = rethrow( () => readFileSync( sourceFilePath, 'utf-8' ), - err => `Error during reading of ${relative( process.cwd(), sourceFilePath )}:\n${err.message}` ); + err => wrapError( `Error during reading of ${relative( process.cwd(), sourceFilePath )}`, err ) ); this.sources = [ { character: 0, fileName: sourceFilePath, line: 1, url, file: new SourceFile( sourceFilePath ) }, ]; diff --git a/packages/plugin-pages/src/theme-plugins/page-tree/a-page-tree-builder.ts b/packages/plugin-pages/src/theme-plugins/page-tree/a-page-tree-builder.ts index 060da5a9..9ea9ba67 100644 --- a/packages/plugin-pages/src/theme-plugins/page-tree/a-page-tree-builder.ts +++ b/packages/plugin-pages/src/theme-plugins/page-tree/a-page-tree-builder.ts @@ -3,7 +3,7 @@ import { resolve } from 'path'; import { DeclarationReflection, ProjectReflection, ReflectionKind, RenderTemplate, RendererEvent, Theme, UrlMapping } from 'typedoc'; -import { rethrow } from '@knodes/typedoc-pluginutils'; +import { rethrow, wrapError } from '@knodes/typedoc-pluginutils'; import { IPluginOptions, IRootPageNode, PageNode } from '../../options'; import type { PagesPlugin } from '../../plugin'; @@ -116,7 +116,7 @@ export abstract class APageTreeBuilder implements IPageTreeBuilder { node.children, rethrow( () => this._getNodeModuleOverride( node ), - err => `Invalid virtual module ${getNodePath( node, parent )}:\n${err.message}`, + err => wrapError( `Invalid virtual module ${getNodePath( node, parent )}`, err ), ) ?? parent, childrenIO ) : []; @@ -151,7 +151,7 @@ export abstract class APageTreeBuilder implements IPageTreeBuilder { const module: ProjectReflection | DeclarationReflection = parent instanceof ProjectReflection ? // If module is project (the default for root), see if workspace is overriden. rethrow( () => this._getNodeModuleOverride( node ), - err => `Invalid node workspace override ${getNodePath( node, parent )}:\n${err.message}`, + err => wrapError( `Invalid node workspace override ${getNodePath( node, parent )}`, err ), ) ?? parent : // Otherwise, we are either in a child page, or in a module. parent instanceof ANodeReflection ? // If child page, inherit module parent.module : // Otherwise, use module as parent @@ -172,7 +172,7 @@ export abstract class APageTreeBuilder implements IPageTreeBuilder { parent, filename, join( io.output, getNodeUrl( node ) ) ), - err => `Could not generate a page reflection for ${getNodePath( node, parent )}:\n${err.message}` ); + err => wrapError( `Could not generate a page reflection for ${getNodePath( node, parent )}`, err ) ); } return new MenuReflection( node.title, diff --git a/packages/plugintestbed/src/index.ts b/packages/plugintestbed/src/index.ts index 6119aa4e..f4b71008 100644 --- a/packages/plugintestbed/src/index.ts +++ b/packages/plugintestbed/src/index.ts @@ -3,3 +3,4 @@ export * from './fs'; export * from './misc'; export * from './mock-markdown-replacer'; export * from './mock-page-memo'; +export * from './run-plugin'; diff --git a/packages/plugintestbed/src/run-plugin.ts b/packages/plugintestbed/src/run-plugin.ts new file mode 100644 index 00000000..fa7156a3 --- /dev/null +++ b/packages/plugintestbed/src/run-plugin.ts @@ -0,0 +1,32 @@ +import assert from 'assert'; +import { resolve } from 'path'; + +import { Many, castArray } from 'lodash'; + +import { Application, ArgumentsReader, TSConfigReader, TypeDocOptions, TypeDocReader } from 'typedoc'; + +export const runPlugin = async ( + testDir: string, + pluginPaths: Many, + { options, output = resolve( testDir, './docs' ) }: {options?: Record; output?: string}= {}, +) => { + const app = new Application(); + app.options.addReader( new ArgumentsReader( 0, [] ) ); + app.options.addReader( new TypeDocReader() ); + app.options.addReader( new TSConfigReader() ); + const baseOptions: Partial = { + treatWarningsAsErrors: true, + plugin: castArray( pluginPaths ), + gitRemote: 'http://example.com', + gitRevision: 'test', + }; + app.bootstrap( { + ...options, + ...baseOptions, + } ); + const project = app.convert(); + expect( project ).toBeTruthy(); + assert( project ); + app.validate( project ); + await app.generateDocs( project, output ); +}; diff --git a/packages/pluginutils/src/current-page-memo.ts b/packages/pluginutils/src/current-page-memo.ts index d70e1de4..6e8e22e5 100644 --- a/packages/pluginutils/src/current-page-memo.ts +++ b/packages/pluginutils/src/current-page-memo.ts @@ -7,13 +7,26 @@ import { PageEvent, Reflection, Renderer } from 'typedoc'; import { ABasePlugin } from './base-plugin'; export class CurrentPageMemo { + private static readonly _plugins = new WeakMap(); private _currentPage?: PageEvent; private _initialized = false; public get initialized(){ return this._initialized; } - public constructor( protected readonly plugin: ABasePlugin ){} + /** + * Get the instance for the given plugin. + * + * @param plugin - The plugin to get memo for, + * @returns the plugin page memo + */ + public static for( plugin: ABasePlugin ){ + const e = this._plugins.get( plugin ) ?? new CurrentPageMemo( plugin ); + this._plugins.set( plugin, e ); + return e; + } + + private constructor( protected readonly plugin: ABasePlugin ){} /** * Start watching for pages event. @@ -31,6 +44,23 @@ export class CurrentPageMemo { } ); } + /** + * Set the current page as being the {@link newPage} while running the {@link callback}. The current page is restored afterwards no matter what. + * + * @param newPage - The page to set. + * @param callback - The function to execute. + * @returns the {@link callback} return value. + */ + public fakeWrapPage( newPage: PageEvent, callback: () => T ){ + const bck = this._currentPage; + this._currentPage = newPage; + try{ + return callback(); + }finally{ + this._currentPage = bck; + } + } + public get currentPage(): PageEvent { assert( this._currentPage ); assert( this._currentPage.model instanceof Reflection ); diff --git a/packages/pluginutils/src/markdown-replacer.spec.ts b/packages/pluginutils/src/markdown-replacer.spec.ts index de2e9e8f..55a21c9b 100644 --- a/packages/pluginutils/src/markdown-replacer.spec.ts +++ b/packages/pluginutils/src/markdown-replacer.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import assert from 'assert'; - import { relative, resolve } from 'path'; import { escapeRegExp } from 'lodash'; @@ -9,9 +8,8 @@ import { Application, DeclarationReflection, MarkdownEvent, ReflectionKind, Sour jest.mock( './base-plugin' ); const { ABasePlugin } = require( './base-plugin' ) as jest.Mocked; -jest.mock( './current-page-memo' ); -const { CurrentPageMemo } = require( './current-page-memo' ) as jest.Mocked; +import { CurrentPageMemo } from './current-page-memo'; import { MarkdownReplacer } from './markdown-replacer'; class TestPlugin extends ABasePlugin { @@ -51,6 +49,9 @@ afterEach( () => { Object.defineProperty( CurrentPageMemo.prototype, 'currentReflection', { writable: true, value: undefined } ); Object.defineProperty( CurrentPageMemo.prototype, 'hasCurrent', { writable: true, value: false } ); } ); +const getParseMarkdownEventListeners = ( plugin: TestPlugin ) => plugin.application.renderer.on.mock.calls + .filter( c => c[0] === MarkdownEvent.PARSE ) + .map( c => c[1] ); describe( MarkdownReplacer.name, () => { describe( 'Once', () => { let plugin: TestPlugin; @@ -70,7 +71,10 @@ describe( MarkdownReplacer.name, () => { const fn = jest.fn().mockReturnValue( '#' ); replacer.bindReplace( /##/g, fn, 'Replacer' ); const evt = new MarkdownEvent( MarkdownEvent.PARSE, source, source ); - plugin.application.renderer.on.mock.calls[0][1]( evt ); + const listeners = getParseMarkdownEventListeners( plugin ); + expect( listeners ).toHaveLength( 1 ); + listeners[0]( evt ); + expect( fn ).toHaveBeenCalledTimes( expectedMaps.length ); expectedMaps.forEach( ( m, i ) => { expect( fn.mock.calls[i][1]() ).toContain( `${m} ` ); expect( fn.mock.calls[i][1]() ).toContain( 'Replacer)' ); @@ -134,7 +138,9 @@ describe( MarkdownReplacer.name, () => { mockCurrentPage( 'Test', resolve( 'hello.ts' ), 1, 1 ); binds.forEach( b => replacer.bindReplace( b.match, b.replacer, b.label ) ); const evt = new MarkdownEvent( MarkdownEvent.PARSE, source, source ); - binds.forEach( ( _b, i ) => plugin.application.renderer.on.mock.calls[i][1]( evt ) ); + const listeners = getParseMarkdownEventListeners( plugin ); + expect( listeners ).toHaveLength( binds.length ); + binds.forEach( ( _b, i ) => listeners[i]( evt ) ); binds.forEach( b => { expect( b.replacer, `Replacer ${b.label}` ).toHaveBeenCalledTimes( b.maps.length ); b.maps.forEach( ( m, j ) => { diff --git a/packages/pluginutils/src/markdown-replacer.ts b/packages/pluginutils/src/markdown-replacer.ts index 7929b59f..d9776ddf 100644 --- a/packages/pluginutils/src/markdown-replacer.ts +++ b/packages/pluginutils/src/markdown-replacer.ts @@ -5,7 +5,7 @@ import { Context, Converter, JSX, MarkdownEvent, SourceFile } from 'typedoc'; import { ABasePlugin } from './base-plugin'; import { CurrentPageMemo } from './current-page-memo'; -import { reflectionSourceUtils, textUtils } from './utils'; +import { reflectionSourceUtils, textUtils, wrapError } from './utils'; interface ISourceEdit { from: number; @@ -74,7 +74,7 @@ export class MarkdownReplacer { private static readonly _mapContainers = new WeakMap(); private readonly _logger = this.plugin.logger.makeChildLogger( 'MarkdownReplacer' ); - private readonly _currentPageMemo = new CurrentPageMemo( this.plugin ); + private readonly _currentPageMemo = CurrentPageMemo.for( this.plugin ); /** * Get the list of source map containers for the given event. @@ -175,8 +175,8 @@ export class MarkdownReplacer { const replacementStr = typeof replacement === 'string' ? replacement : JSX.renderElement( replacement ); thisContainer.editions.push( { from: index, to: index + fullMatch.length, replacement: replacementStr, source: fullMatch } ); return replacementStr; - } catch( e ){ - throw new Error( `Error in ${getSourceHint()}:\n${e}` ); + } catch( e: any ){ + throw wrapError( `Error in ${getSourceHint()}`, e ); } } ); MarkdownReplacer._mapContainers.set( event, [ diff --git a/packages/pluginutils/src/path-reflection-resolver.ts b/packages/pluginutils/src/path-reflection-resolver.ts index 3ffdff1c..e4c62d44 100644 --- a/packages/pluginutils/src/path-reflection-resolver.ts +++ b/packages/pluginutils/src/path-reflection-resolver.ts @@ -3,11 +3,28 @@ import { existsSync } from 'fs'; import { dirname, isAbsolute, join, resolve } from 'path'; import { isString, uniq } from 'lodash'; +import { sync as pkgUpSync } from 'pkg-up'; import { LiteralUnion } from 'type-fest'; import { DeclarationReflection, ProjectReflection, Reflection, ReflectionKind } from 'typedoc'; import type { ABasePlugin } from './base-plugin'; +const getReflectionSources = ( reflection: Reflection, isDotPrefixed: boolean ) => { + const selfSources = reflection.sources + ?.map( s => s?.file?.fullFileName ?? s?.fileName ) + .filter( isString ) + .map( p => resolve( p ) ) ?? []; + if( !isDotPrefixed && reflection instanceof DeclarationReflection && reflection.kindOf( ReflectionKind.Module ) ){ + const pkgSources = uniq( selfSources.map( s => pkgUpSync( { cwd: dirname( s ) } ) ).filter( isString ) ); + return [ + ...pkgSources, + ...selfSources, + ]; + } else { + return selfSources; + } +}; + /** * Don't worry about typings, it's just a string with special prefixes. */ @@ -45,20 +62,11 @@ export class PathReflectionResolver { * @returns the resolved path. */ public resolveAllFromReflection( reflection: Reflection, path: string ): string[]{ - if( isAbsolute( path ) ){ - return [ path ]; - } - return reflection.sources - ?.map( s => { - const source = s?.fileName; - if( !source ){ - return undefined; - } else { - return resolve( dirname( source ), path ); - } - } ) - .filter( isString ) - .filter( existsSync ) ?? []; + const paths = isAbsolute( path ) ? + [ path ] : + getReflectionSources( reflection, path.startsWith( '.' ) ) + .map( s => resolve( dirname( s ), path ) ); + return paths.filter( existsSync ); } /** @@ -96,10 +104,6 @@ export class PathReflectionResolver { if( path.startsWith( '~~/' ) ){ path = path.replace( /^~~\//, '' ); currentReflection = project; - if( containerFolder ){ - pathsToTry.push( join( containerFolder, path ) ); - } - pathsToTry.push( path ); } else if( path.match( /^~.+\// ) ){ const workspace = this.getWorkspaces( project ).slice( 1 ).find( w => path.startsWith( `~${w.name}/` ) ); if( !workspace ){ @@ -107,22 +111,22 @@ export class PathReflectionResolver { } path = path.slice( workspace.name.length + 2 ); currentReflection = workspace; - if( containerFolder ){ - pathsToTry.push( join( containerFolder, path ) ); - } - pathsToTry.push( path ); - } else { - pathsToTry.push( path ); } + if( containerFolder && !path.startsWith( '.' ) ){ + pathsToTry.push( join( containerFolder, path ) ); + } + pathsToTry.push( path ); const reflection = currentReflection ?? project; - for( const p of uniq( pathsToTry ).reverse() ){ + const pathsList = uniq( pathsToTry ); + for( const p of pathsList ){ const ret = this.resolveAllFromReflection( reflection, p ); if( ret.length > 0 ) { return ret[0]; } } - const sourcesLog = `Reflection sources: ${JSON.stringify( reflection.sources?.map( s => s.fileName ) ?? [] )}`; - this.plugin.logger.error( `Could not resolve "${pathSv}" from reflection "${reflection.name}". ${sourcesLog}` ); + const sourcesLog = `Reflection sources: ${JSON.stringify( getReflectionSources( reflection, path.startsWith( '.' ) ).map( this.plugin.relativeToRoot.bind( this.plugin ) ) )}`; + this.plugin.logger.error( `Could not resolve "${pathSv}" from reflection "${reflection.name}". Tried paths: ${JSON.stringify( pathsList + .map( p => isAbsolute( p ) ? this.plugin.relativeToRoot( p ) : p ) )}. ${sourcesLog}` ); return undefined; } } diff --git a/packages/pluginutils/src/utils/misc.ts b/packages/pluginutils/src/utils/misc.ts index efdc23cf..72bcbb56 100644 --- a/packages/pluginutils/src/utils/misc.ts +++ b/packages/pluginutils/src/utils/misc.ts @@ -24,3 +24,13 @@ export const rethrow = ( block: () => T, newErrorFactory: ( err: any ) => str } } }; + +export const wrapError = ( message: string, err: any, propagateStack = true ) => { + const newErr = new Error( `${message}:\n${err.message || err}` ); + if( propagateStack ){ + if( 'stack' in err && err.stack ){ + newErr.stack = err.stack; + } + } + return newErr; +};