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.
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...
@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.
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"
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.
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 |
Issue Title | Created Date | Updated Date |
---|