How do you get a reference to the current file path in a plugin?

This issue has been tracked since 2023-01-27.

I'm trying to create a plugin to embed the contents of a separate file inside a script block. For example:

/**
 * The Search Results component encapsulates all components relating to search-results.
 *
 * # Sample Template
 * Here is a sample template:
 * @embed ./sample-template.html
 */
export class SampleComponent {
}

I thought I had this working, and it does if I run TypeDoc against the file containing this single class directly rather than pointing it at the root index.js file of my project. This is the code I'm using:

import { readFileSync, existsSync } from 'fs';
import * as path from 'path';
import { Application, Context, Converter, ReflectionKind } from 'typedoc';

export function load(app: Application) {
    app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (context: Context) => {
        const reflections = context.project.getReflectionsByKind(ReflectionKind.All);

        for (const reflection of reflections) {
            const comment = reflection.comment;

            comment?.blockTags
                ?.filter((t) => t.tag === '@embed')
                .forEach((t) => {
                    if (!context.scope.sources?.length) {
                        console.log(context.scope.sources ?? 'NO SOURCES LOADED?!');

                        return;
                    }

                    const currentDirectoryPath = context.scope.sources?.[0]?.fullFileName?.replace(/[/\\][^/\\]*$/, '');
                    const embedRelativePath = t.content?.[0]?.text;

                    if (!currentDirectoryPath || !embedRelativePath) {
                        return;
                    }

                    const embedAbsolutePath = path.join(currentDirectoryPath, embedRelativePath);

                    if (!existsSync(embedAbsolutePath)) {
                        throw new Error(`@embed file at ${embedAbsolutePath} does not exist`);
                    }

                    const content = readFileSync(embedAbsolutePath).toString();

                    t.content = [
                        {
                            kind: 'code',
                            text: `\`\`\`\n${content}\n\`\`\``
                        }
                    ];
                });
        }
    });
}

When I run this against index.ts which pulls in many different classes (though only one with my @embed tag), context.scope.sources is null.

Any guidance on how to get the current file path would be greatly appreciated. The documentation around plugin development is quite limited, so I only came up with this approach through looking at the source of some other, not relevant plugins and poking around at the context object and seeing what might be useful.

jkasperbl wrote this answer on 2023-01-30

I figured it out. Instead of using context.scope.sources?.[0]?.fullFileName, I need to use reflection.sources?.[0]?.fullFileName.

Gerrit0 wrote this answer on 2023-02-01

It's worth mentioning that the sources property will not be set if the user passed --disableSources. It's safer to get the ts.Symbol from reflection.project.getSymbolFromReflection. Also worth mentioning that "current file" isn't something well defined. You could say "file containing the declaration for this member"... but how does that play with inherited members from a class in another file? How does that play with declaration merged members which are defined in different files? What if that member used @inheritDoc to copy a comment from some declaration in yet another file?... probably edge cases that don't apply if you're building a plugin just for yourself, but for general purpose ones...

jkasperbl wrote this answer on 2023-02-02

@Gerrit0 Thanks for your insight!

By "current file", I'm referring to the file that contains the comment containing my @embed block. That's an interesting scenario though, and you're correct that my logic probably wouldn't handle that. Do you have any suggestions on how to approach that? The original intent was just to create a simple plugin for internal use, but I'm not opposed to potentially building it out to be useful to others.

Gerrit0 wrote this answer on 2023-02-04

It depends on when you're handling the @embed, if you're doing it before Converter.EVENT_RESOLVE_BEGIN (or before TypeDoc's listener on that), then @inheritDoc wont have been resolved, so you can avoid that pitfall.

That unfortunately still doesn't cover everything because TypeDoc supports comments in some... weird... locations, but I'm not sure of a good way to handle that...

/** Doc comment here may be attached to `Foo` */
export { Foo } from "./foo"
jkasperbl wrote this answer on 2023-02-18

Thanks for the response! It probably doesn't resolve the concern you raised earlier, but I ended up with the following that meets my use cases at least:

import { existsSync, readFileSync } from 'fs';
import * as path from 'path';
import { Application, Context, Converter, ReflectionKind } from 'typedoc';

export function load(app: Application) {
    app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (context: Context) => {
        const reflections = context.project.getReflectionsByKind(ReflectionKind.All);

        for (const reflection of reflections) {
            const comment = reflection.comment;
            const sourceFileAbsolutePath = reflection.sources?.[0]?.fullFileName;

            if (!comment || !sourceFileAbsolutePath) {
                continue;
            }

            comment.summary
                .filter((i) => i.kind === 'inline-tag' && (i as any).tag === '@embed')
                .forEach((i) => {
                    const embedRelativePath = i.text?.trim();

                    if (!embedRelativePath) {
                        throw new Error(`@embed missing file path in ${reflection.getFriendlyFullName()}`);
                    }

                    const sourceDirectoryAbsolutePath = path.parse(sourceFileAbsolutePath).dir;
                    const embedAbsolutePath = path.join(sourceDirectoryAbsolutePath, embedRelativePath);

                    if (!existsSync(embedAbsolutePath)) {
                        throw new Error(`@embed file at ${embedAbsolutePath} does not exist in ${reflection.getFriendlyFullName()}`);
                    }

                    const embedCode = readFileSync(embedAbsolutePath).toString();

                    delete (i as any).tag;

                    i.kind = 'code';
                    i.text = `\`\`\`\n${embedCode}\n\`\`\``;
                });
        }
    });
}

However, I do have one issue and I'm not sure if it's a bug or just something I don't understand. I set disableSources to true in my typedoc.json file to remove the links to the source files in my generated documentation, but that breaks my plugin as the reflection.sources property is always undefined. I've had to revert that change for the time being, but if you have any insight on this that would be appreciated.

More Details About Repo
Owner Name TypeStrong
Repo Name typedoc
Full Name TypeStrong/typedoc
Language TypeScript
Created Date 2014-05-24
Updated Date 2023-03-19
Star Count 6487
Watcher Count 68
Fork Count 639
Issue Count 48

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date