Skip to content

Commit cff350a

Browse files
authored
Fix broken symlinks prevents the site to be started (#566)
* Avoid exception when symlink can't be resolved * Add unit test to cover broken symlinks case
1 parent 1438990 commit cff350a

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎src/lib/symlink-manager.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path';
33
import { createNodeFsMountHandler } from '@php-wasm/node';
44
import { PHP, UnmountFunction } from '@php-wasm/universal';
55
import { pathExists } from './fs-utils';
6+
import { isErrnoException } from './is-errno-exception';
67

78
type MountedTarget = {
89
preExisting: boolean;
@@ -137,7 +138,17 @@ export class SymlinkManager {
137138
*/
138139
private async addSymlink( filename: string ) {
139140
const fullPath = path.join( this.projectPath, filename );
140-
const target = await fs.realpath( fullPath );
141+
let target: string;
142+
try {
143+
target = await fs.realpath( fullPath );
144+
} catch ( err ) {
145+
if ( isErrnoException( err ) && err.code === 'ENOENT' ) {
146+
console.error( `Symlink target does not exist: ${ fullPath }` );
147+
return;
148+
}
149+
console.error( err );
150+
return;
151+
}
141152

142153
if ( ! this.php.requestHandler ) {
143154
throw new Error( 'Request handler is not set' );

‎src/lib/tests/symlink-manage.test.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,49 @@ describe( 'SymlinkManager', () => {
140140
( symlinkManager as unknown as SymlinkManagerPrivateProperties ).mountedTargets.size
141141
).toBe( 1 );
142142
} );
143+
144+
it( 'should handle broken symlinks', async () => {
145+
const mockSymlink = '/mock/project/path/broken/symlink';
146+
const relativeSymlinkPath = 'broken/symlink';
147+
148+
mockPHP.fileExists.mockImplementation( ( path ) => ! path.includes( 'vfspath' ) );
149+
mockPHP.readlink.mockImplementation( ( path ) => '/mock/document/root/vfspath/' + path );
150+
151+
// Mock fs.readdir to return our mock symlinks
152+
( fs.readdir as jest.Mock ).mockResolvedValue( path.basename( mockSymlink ) );
153+
154+
// Mock fs.lstat to indicate these are symlinks
155+
( fs.lstat as jest.Mock ).mockResolvedValue( { isSymbolicLink: () => true } as Stats );
156+
157+
// Mock fs.realpath to throw ENOENT error, simulating a broken symlink
158+
const notFoundError = new Error( 'ENOENT' ) as NodeJS.ErrnoException;
159+
notFoundError.code = 'ENOENT';
160+
( fs.realpath as jest.Mock ).mockRejectedValue( notFoundError );
161+
162+
// Spy on console.error
163+
const consoleErrorSpy = jest.spyOn( console, 'error' ).mockImplementation();
164+
165+
await symlinkManager.scanAndCreateSymlinks();
166+
167+
// Verify that the broken symlink was not added to the internal map
168+
expect(
169+
( symlinkManager as unknown as SymlinkManagerPrivateProperties ).symlinks.has(
170+
relativeSymlinkPath
171+
)
172+
).toBe( false );
173+
174+
// Verify that no mount was attempted for the broken symlink
175+
expect( mockPHP.mkdir ).not.toHaveBeenCalled();
176+
expect( mockPHP.mount ).not.toHaveBeenCalled();
177+
178+
// Verify that an error was logged
179+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
180+
expect.stringContaining( 'Symlink target does not exist:' )
181+
);
182+
183+
// Clean up the spy
184+
consoleErrorSpy.mockRestore();
185+
} );
143186
} );
144187

145188
describe( 'startWatching and stopWatching', () => {

0 commit comments

Comments
 (0)