typedoc icon indicating copy to clipboard operation
typedoc copied to clipboard

Watch Inclusions

Open Metaxona opened this issue 1 year ago • 3 comments

Search terms

Watch

Expected Behavior

I expected watch to watch the files changes on

  • typedoc.json
  • config.projectDocuments files
  • readme -> when not none

Actual Behavior

changes made to the said files included above does not re-execute the build unless i quit watch mode and re-execute the command again

Steps to reproduce the bug

if this is not a normal behavior tag me on this issue so i can provide a repo that recreates this bug, if this is the normal behavior converting this to a feature request would be the next choice

Environment

  • Typedoc version: ^0.26.5
  • TypeScript version: ^5.5.4
  • Node.js version: v20.11.0
  • OS: Ubuntu 22.04.4 LTS x86 (Specifics: Ubuntu Server)

Metaxona avatar Aug 19 '24 22:08 Metaxona

This is expected as watch mode just uses typescript's watch with a hook to generate docs, there isn't any support for watching other changes. This is essentially the same limitation as what is preventing #1772

Gerrit0 avatar Aug 19 '24 22:08 Gerrit0

so currently there is no way to solve it?

Metaxona avatar Aug 19 '24 23:08 Metaxona

Correct, it isn't supported yet

Gerrit0 avatar Aug 20 '24 00:08 Gerrit0

For anyone encountering this issue, you can work around it using the onchange package from npm, and a build script like this:

"watch-docs": "onchange --await-write-finish 3000 -i --kill \"*.md\"  \"guides/**/*.md\" \"typedoc.config.*\" \"typedoc/*\" -- typedoc --watch",

Of course you need to change the patterns and command line to something that makes sense for your project. (e.g. the typedoc/* part is to catch changes to my custom CSS, and you might not even have such a directory.)

The --await-write-finish bit says to wait 3 seconds after a change to these files to make sure you're not changing something else, to avoid continually killing and restarting the process (e.g. if you have a build step that writes to your .md files or custom.css.)

pjeby avatar Dec 07 '24 22:12 pjeby

If this isn't fixable, maybe a quick note about it in the docs would help?

shawninder avatar Dec 17 '24 15:12 shawninder

There is a note - https://typedoc.org/documents/Options.Other.html#watch

Note: This mode will only detect changes to files watched by the TypeScript compiler. Changes to other files (README.md, imported files with @include or @includeCode) will not cause a rebuild.

Gerrit0 avatar Dec 18 '24 03:12 Gerrit0

I did a little experimentation with the TS compiler API, and it has a way to add files to watch and get a callback when the files change.

diff --git a/src/lib/application.ts b/src/lib/application.ts
index e17243c1..7a13ba39 100644
--- a/src/lib/application.ts
+++ b/src/lib/application.ts
@@ -506,6 +506,7 @@ export class Application extends AbstractComponent<

         let successFinished = true;
         let currentProgram: ts.Program | undefined;
+        let watches: ts.FileWatcher[] = []

         const runSuccess = () => {
             if (!currentProgram) {
@@ -528,6 +529,15 @@ export class Application extends AbstractComponent<
                     return;
                 }
                 const project = this.converter.convert(entryPoints);
+                watches.forEach(w => w.close())
+                watches = []
+                for(const d of project.documents || []) {
+                    const path = project.files.getReflectionPath(d)
+                    console.log("watching", path)
+                    if (path) watches.push(host.watchFile(path, () => {
+                        console.log("changed", path)
+                    }))
+                }
                 currentProgram = undefined;
                 successFinished = false;
                 void success(project).then(() => {

With the above patch, I was able to get TS to output "changed" notices for files that were document sources. I'm not entirely sure where to take the patch from there because the intended logic of runSuccess() eludes me a bit here - it has a lot of self-reference and state flags. (And presumably it would make sense to only update the changed document reflections and then rerun the success part, rather than rerunning the converter, if the currentProgram hasn't changed.) Last, but not least, there's a bit of a headache in the form of needing debouncing, as the watch callback can be invoked 3 or 4 times for one save of a changed file.

Any hints on how to expand this into an acceptable PR would be welcome, as I'd love to have the feature. My current impression is that runSuccess is the way it is because it's in some sense trying to intelligently merge two async producers: the output from the TypeScript compiler, and the async operation of the output generation. That is, it can get a call from the afterProgramCreate callback while an output generation is in progress, and so needs to not run a second success operation while the first is in progress.

So, if I'm still understanding correctly, a debounced watch callback would want to check if there's a currentProgram, and if so, ignore the notice, because it's going to do a full regen anyway. And if successFinished is false, it needs to wait for the last output generation and then start a new one. (After first either re-running the convert step or just recompiling the changed document reflection somehow.)

Does that sound right?

pjeby avatar Jan 26 '25 06:01 pjeby

Got this version working, at least on a small project:

diff --git a/src/lib/application.ts b/src/lib/application.ts
index e17243c1..f38fa442 100644
--- a/src/lib/application.ts
+++ b/src/lib/application.ts
@@ -506,6 +506,7 @@ export class Application extends AbstractComponent<

         let successFinished = true;
         let currentProgram: ts.Program | undefined;
+        let watches: ts.FileWatcher[] = []

         const runSuccess = () => {
             if (!currentProgram) {
@@ -528,6 +529,18 @@ export class Application extends AbstractComponent<
                     return;
                 }
                 const project = this.converter.convert(entryPoints);
+                watches.forEach(w => w.close())
+                watches = []
+                const lastProgram = currentProgram;
+                for(const d of project.documents || []) {
+                    const path = project.files.getReflectionPath(d)
+                    if (path) watches.push(host.watchFile(path, () => {
+                        if (!currentProgram) {
+                            currentProgram = lastProgram
+                            if (successFinished) runSuccess()
+                        }
+                    }))
+                }
                 currentProgram = undefined;
                 successFinished = false;
                 void success(project).then(() => {

The idea is it fakes the arrival of a new currentProgram (by setting it to the previous one) if there isn't one already (because if there is, a rerun is already scheduled), and restarts generation if it isn't already running.

Presumably this could be extended in a straightforward way to handle at least the README, but config files would require restarting the whole program I think.

(Edit note: tweaked the logic slightly so document changes happening during the output writing will still flag the need for another pass.)

pjeby avatar Jan 26 '25 07:01 pjeby