diff --git a/notifiers/balloon.js b/notifiers/balloon.js index df56976..d38010b 100644 --- a/notifiers/balloon.js +++ b/notifiers/balloon.js @@ -22,8 +22,7 @@ Usage 4 = Closed or faded out */ -const path = require( 'path' ); -const notifier = path.resolve( __dirname, '../vendor/notifu/notifu' ); +const notifier = require.resolve( '../vendor/notifu/notifu.exe' ); // Require resolve will use relative pathing correctly const checkGrowl = require( '../lib/checkGrowl' ); const utils = require( '../lib/utils' ); const Toaster = require( './toaster' ); @@ -126,7 +125,8 @@ function doNotification( options, notifierOptions, callback ) options = utils.mapToNotifu( options ); options.p = options.p || 'Example Notification:'; - const fullNotifierPath = notifier + ( is64Bit ? '64' : '' ) + '.exe'; + // Strip .exe extension from resolved path before adding 64-bit suffix, then re-add .exe + const fullNotifierPath = notifier.replace( /\.exe$/i, '' ) + ( is64Bit ? '64' : '' ) + '.exe'; const localNotifier = notifierOptions.customPath || fullNotifierPath; // console.log('Loading ' + fullNotifierPath); diff --git a/notifiers/notificationcenter.js b/notifiers/notificationcenter.js index 8287b63..2b1b552 100644 --- a/notifiers/notificationcenter.js +++ b/notifiers/notificationcenter.js @@ -3,8 +3,7 @@ */ const utils = require( '../lib/utils' ); const Growl = require( './growl' ); -const path = require( 'path' ); -const notifier = path.join( __dirname, '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier' ); +const notifier = require.resolve( '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier' ); // Require resolve will use relative pathing correctly const EventEmitter = require( 'events' ).EventEmitter; const util = require( 'util' ); diff --git a/notifiers/toaster.js b/notifiers/toaster.js index a2ebea6..8816900 100644 --- a/notifiers/toaster.js +++ b/notifiers/toaster.js @@ -2,8 +2,7 @@ Wrapper for the toaster (https://github.com/nels-o/toaster) */ -const path = require( 'path' ); -const notifier = path.resolve( __dirname, '../vendor/ntfyToast/ntfytoast' ); +const notifier = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); // Require resolve will use relative pathing correctly const utils = require( '../lib/utils' ); const Balloon = require( './balloon' ); const crypto = require( 'crypto' ); @@ -141,7 +140,8 @@ function notifyRaw( options, callback ) resultBuffer = out; options.pipeName = server.namedPipe; - const localNotifier = options.customPath || this.options.customPath || notifier + '.exe'; + // No need to append .exe since require.resolve() already includes the full extension + const localNotifier = options.customPath || this.options.customPath || notifier; // options.customPath || this.options.customPath || notifier + '-x' + (is64Bit ? '64' : '86') + '.exe'; options = utils.mapToWin8( options ); diff --git a/test/module-resolution.js b/test/module-resolution.js new file mode 100644 index 0000000..91a0441 --- /dev/null +++ b/test/module-resolution.js @@ -0,0 +1,186 @@ +/** + * Tests for module path resolution using require.resolve() + * These tests verify that vendor binaries are correctly resolved + * regardless of how the module is installed or bundled. + */ + +const path = require( 'path' ); +const fs = require( 'fs' ); + +describe( 'Module Path Resolution', () => +{ + describe( 'Vendor binary paths', () => + { + it( 'should resolve Windows Toast (ntfytoast.exe) path correctly', () => + { + const toasterPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + + expect( toasterPath ).toBeTruthy(); + expect( toasterPath ).toContain( 'ntfytoast.exe' ); + expect( path.isAbsolute( toasterPath ) ).toBeTruthy(); + }); + + it( 'should resolve Windows Balloon (notifu.exe) path correctly', () => + { + const notifuPath = require.resolve( '../vendor/notifu/notifu.exe' ); + + expect( notifuPath ).toBeTruthy(); + expect( notifuPath ).toContain( 'notifu.exe' ); + expect( path.isAbsolute( notifuPath ) ).toBeTruthy(); + }); + + it( 'should resolve macOS terminal-notifier path correctly', () => + { + const terminalNotifierPath = require.resolve( '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier' ); + + expect( terminalNotifierPath ).toBeTruthy(); + expect( terminalNotifierPath ).toContain( 'terminal-notifier' ); + expect( path.isAbsolute( terminalNotifierPath ) ).toBeTruthy(); + }); + }); + + describe( 'Path resolution consistency', () => + { + it( 'should resolve paths identically when called multiple times', () => + { + const path1 = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + const path2 = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + + expect( path1 ).toBe( path2 ); + }); + + it( 'should resolve to actual file system locations', () => + { + const toasterPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + const notifuPath = require.resolve( '../vendor/notifu/notifu.exe' ); + const terminalNotifierPath = require.resolve( '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier' ); + + // At least one should exist depending on platform + const paths = [ toasterPath, notifuPath, terminalNotifierPath ]; + const existingPaths = paths.filter( ( p ) => fs.existsSync( p ) ); + + expect( existingPaths.length ).toBeGreaterThan( 0 ); + }); + }); + + describe( 'Bundler/Webpack compatibility', () => + { + it( 'should resolve without relying on __dirname', () => + { + // Simulate webpack/bundler environment where __dirname might be altered + const originalDirname = __dirname; + + // require.resolve should still work correctly + const toasterPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + + expect( toasterPath ).toBeTruthy(); + expect( path.isAbsolute( toasterPath ) ).toBeTruthy(); + + // Verify __dirname is still intact (we didn't actually modify it) + expect( __dirname ).toBe( originalDirname ); + }); + + it( 'should resolve relative to module location, not caller location', () => + { + // require.resolve() is relative to this file's location + const resolvedPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + + // The resolved path should contain the vendor directory + expect( resolvedPath ).toContain( 'vendor' ); + expect( resolvedPath ).toContain( 'ntfyToast' ); + }); + + it( 'should handle symlinked node_modules correctly', () => + { + // require.resolve follows symlinks and resolves to real paths + const toasterPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + const realPath = fs.realpathSync( toasterPath ); + + // Both should be absolute paths + expect( path.isAbsolute( toasterPath ) ).toBeTruthy(); + expect( path.isAbsolute( realPath ) ).toBeTruthy(); + }); + }); + + describe( 'Notifier implementations use correct paths', () => + { + it( 'should have Toaster using require.resolve()', () => + { + // Read the toaster.js file to verify it uses require.resolve + const toasterSource = fs.readFileSync( + path.join( __dirname, '../notifiers/toaster.js' ), + 'utf8' + ); + + expect( toasterSource ).toContain( 'require.resolve' ); + expect( toasterSource ).toContain( '../vendor/ntfyToast/ntfytoast.exe' ); + }); + + it( 'should have Balloon using require.resolve()', () => + { + const balloonSource = fs.readFileSync( + path.join( __dirname, '../notifiers/balloon.js' ), + 'utf8' + ); + + expect( balloonSource ).toContain( 'require.resolve' ); + expect( balloonSource ).toContain( '../vendor/notifu/notifu.exe' ); + }); + + it( 'should have NotificationCenter using require.resolve()', () => + { + const ncSource = fs.readFileSync( + path.join( __dirname, '../notifiers/notificationcenter.js' ), + 'utf8' + ); + + expect( ncSource ).toContain( 'require.resolve' ); + expect( ncSource ).toContain( 'terminal-notifier' ); + }); + + it( 'should not use path.resolve() with __dirname for vendor binaries', () => + { + const toasterSource = fs.readFileSync( + path.join( __dirname, '../notifiers/toaster.js' ), + 'utf8' + ); + const balloonSource = fs.readFileSync( + path.join( __dirname, '../notifiers/balloon.js' ), + 'utf8' + ); + const ncSource = fs.readFileSync( + path.join( __dirname, '../notifiers/notificationcenter.js' ), + 'utf8' + ); + + // Check that old pattern (path.resolve(__dirname, '../vendor/...')) is not present + const oldPattern = /path\.(resolve|join)\s*\(\s*__dirname.*vendor/; + + expect( oldPattern.test( toasterSource ) ).toBeFalsy(); + expect( oldPattern.test( balloonSource ) ).toBeFalsy(); + expect( oldPattern.test( ncSource ) ).toBeFalsy(); + }); + }); + + describe( 'Extension handling', () => + { + it( 'should include .exe extension in resolved Windows paths', () => + { + const toasterPath = require.resolve( '../vendor/ntfyToast/ntfytoast.exe' ); + const notifuPath = require.resolve( '../vendor/notifu/notifu.exe' ); + + expect( path.extname( toasterPath ) ).toBe( '.exe' ); + expect( path.extname( notifuPath ) ).toBe( '.exe' ); + }); + + it( 'should handle paths with multiple dots correctly', () => + { + const terminalNotifierPath = require.resolve( + '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier' + ); + + expect( terminalNotifierPath ).toContain( 'mac.noindex' ); + expect( terminalNotifierPath ).toContain( '.app' ); + }); + }); +});