1+ const { Plugin, Notice, debounce} = require ( "obsidian" ) ;
2+ const fs = require ( "fs" ) ;
3+
4+ const watchNeeded = window . process . platform !== "darwin" && window . process . platform !== "win32" ;
5+
6+ module . exports = class HotReload extends Plugin {
7+
8+ statCache = new Map ( ) ; // path -> Stat
9+ queue = Promise . resolve ( ) ;
10+
11+ run ( val , err ) {
12+ return this . queue = this . queue . then ( val , err ) ;
13+ }
14+
15+ reindexPlugins = debounce ( ( ) => this . run ( ( ) => this . getPluginNames ( ) ) , 500 , true ) ;
16+ requestScan = debounce ( ( ) => this . run ( ( ) => this . checkVersions ( ) ) , 250 , true ) ;
17+
18+ onload ( ) {
19+ app . workspace . onLayoutReady ( async ( ) => {
20+ this . pluginReloaders = { } ;
21+ this . inProgress = null ;
22+ await this . getPluginNames ( ) ;
23+ this . registerEvent ( this . app . vault . on ( "raw" , this . requestScan ) ) ;
24+ this . watch ( ".obsidian/plugins" ) ;
25+ this . requestScan ( ) ;
26+ this . addCommand ( {
27+ id : "scan-for-changes" ,
28+ name : "Check plugins for changes and reload them" ,
29+ callback : ( ) => this . requestScan ( )
30+ } )
31+ } ) ;
32+ }
33+
34+ watch ( path ) {
35+ if ( this . app . vault . adapter . watchers . hasOwnProperty ( path ) ) return ;
36+ const realPath = [ this . app . vault . adapter . basePath , path ] . join ( "/" ) ;
37+ const lstat = fs . lstatSync ( realPath ) ;
38+ if ( lstat && ( watchNeeded || lstat . isSymbolicLink ( ) ) && fs . statSync ( realPath ) . isDirectory ( ) ) {
39+ this . app . vault . adapter . startWatchPath ( path , false ) ;
40+ }
41+ }
42+
43+ async checkVersions ( ) {
44+ const base = this . app . plugins . getPluginFolder ( ) ;
45+ for ( const dir of Object . keys ( this . pluginNames ) ) {
46+ for ( const file of [ "manifest.json" , "main.js" , "styles.css" , ".hotreload" ] ) {
47+ const path = `${ base } /${ dir } /${ file } ` ;
48+ const stat = await app . vault . adapter . stat ( path ) ;
49+ if ( stat ) {
50+ if ( this . statCache . has ( path ) && stat . mtime !== this . statCache . get ( path ) . mtime ) {
51+ this . onFileChange ( path ) ;
52+ }
53+ this . statCache . set ( path , stat ) ;
54+ }
55+ }
56+ }
57+ }
58+
59+ async getPluginNames ( ) {
60+ const plugins = { } , enabled = new Set ( ) ;
61+ for ( const { id, dir} of Object . values ( app . plugins . manifests ) ) {
62+ this . watch ( dir ) ;
63+ plugins [ dir . split ( "/" ) . pop ( ) ] = id ;
64+ if (
65+ await this . app . vault . exists ( dir + "/.git" ) ||
66+ await this . app . vault . exists ( dir + "/.hotreload" )
67+ ) enabled . add ( id ) ;
68+ }
69+ this . pluginNames = plugins ;
70+ this . enabledPlugins = enabled ;
71+ }
72+
73+ onFileChange ( filename ) {
74+ if ( ! filename . startsWith ( this . app . plugins . getPluginFolder ( ) + "/" ) ) return ;
75+ const path = filename . split ( "/" ) ;
76+ const base = path . pop ( ) , dir = path . pop ( ) ;
77+ if ( path . length === 1 && dir === "plugins" ) return this . watch ( filename ) ;
78+ if ( path . length != 2 ) return ;
79+ const plugin = dir && this . pluginNames [ dir ] ;
80+ if ( base === "manifest.json" || base === ".hotreload" || base === ".git" || ! plugin ) return this . reindexPlugins ( ) ;
81+ if ( base !== "main.js" && base !== "styles.css" ) return ;
82+ if ( ! this . enabledPlugins . has ( plugin ) ) return ;
83+ const reloader = this . pluginReloaders [ plugin ] || (
84+ this . pluginReloaders [ plugin ] = debounce ( ( ) => this . run ( ( ) => this . reload ( plugin ) , console . error ) , 750 , true )
85+ ) ;
86+ reloader ( ) ;
87+ }
88+
89+ async reload ( plugin ) {
90+ const plugins = app . plugins ;
91+
92+ // Don't reload disabled plugins
93+ if ( ! plugins . enabledPlugins . has ( plugin ) ) return ;
94+
95+ await plugins . disablePlugin ( plugin ) ;
96+ console . debug ( "disabled" , plugin ) ;
97+
98+ // Ensure sourcemaps are loaded (Obsidian 14+)
99+ const oldDebug = localStorage . getItem ( "debug-plugin" ) ;
100+ localStorage . setItem ( "debug-plugin" , "1" ) ;
101+ try {
102+ await plugins . enablePlugin ( plugin ) ;
103+ } finally {
104+ // Restore previous setting
105+ if ( oldDebug === null ) localStorage . removeItem ( "debug-plugin" ) ; else localStorage . setItem ( "debug-plugin" , oldDebug ) ;
106+ }
107+ console . debug ( "enabled" , plugin ) ;
108+ new Notice ( `Plugin "${ plugin } " has been reloaded` ) ;
109+ }
110+ }
0 commit comments