Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/lib/symlink-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path';
import { createNodeFsMountHandler } from '@php-wasm/node';
import { PHP, UnmountFunction } from '@php-wasm/universal';
import { pathExists } from './fs-utils';
import { isErrnoException } from './is-errno-exception';

type MountedTarget = {
preExisting: boolean;
Expand Down Expand Up @@ -137,7 +138,17 @@ export class SymlinkManager {
*/
private async addSymlink( filename: string ) {
const fullPath = path.join( this.projectPath, filename );
const target = await fs.realpath( fullPath );
let target: string;
try {
target = await fs.realpath( fullPath );
} catch ( err ) {
if ( isErrnoException( err ) && err.code === 'ENOENT' ) {
console.error( `Symlink target does not exist: ${ fullPath }` );
return;
}
console.error( err );
return;
}

if ( ! this.php.requestHandler ) {
throw new Error( 'Request handler is not set' );
Expand Down
43 changes: 43 additions & 0 deletions src/lib/tests/symlink-manage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,49 @@ describe( 'SymlinkManager', () => {
( symlinkManager as unknown as SymlinkManagerPrivateProperties ).mountedTargets.size
).toBe( 1 );
} );

it( 'should handle broken symlinks', async () => {
const mockSymlink = '/mock/project/path/broken/symlink';
const relativeSymlinkPath = 'broken/symlink';

mockPHP.fileExists.mockImplementation( ( path ) => ! path.includes( 'vfspath' ) );
mockPHP.readlink.mockImplementation( ( path ) => '/mock/document/root/vfspath/' + path );

// Mock fs.readdir to return our mock symlinks
( fs.readdir as jest.Mock ).mockResolvedValue( path.basename( mockSymlink ) );

// Mock fs.lstat to indicate these are symlinks
( fs.lstat as jest.Mock ).mockResolvedValue( { isSymbolicLink: () => true } as Stats );

// Mock fs.realpath to throw ENOENT error, simulating a broken symlink
const notFoundError = new Error( 'ENOENT' ) as NodeJS.ErrnoException;
notFoundError.code = 'ENOENT';
( fs.realpath as jest.Mock ).mockRejectedValue( notFoundError );

// Spy on console.error
const consoleErrorSpy = jest.spyOn( console, 'error' ).mockImplementation();

await symlinkManager.scanAndCreateSymlinks();

// Verify that the broken symlink was not added to the internal map
expect(
( symlinkManager as unknown as SymlinkManagerPrivateProperties ).symlinks.has(
relativeSymlinkPath
)
).toBe( false );

// Verify that no mount was attempted for the broken symlink
expect( mockPHP.mkdir ).not.toHaveBeenCalled();
expect( mockPHP.mount ).not.toHaveBeenCalled();

// Verify that an error was logged
expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect.stringContaining( 'Symlink target does not exist:' )
);

// Clean up the spy
consoleErrorSpy.mockRestore();
} );
} );

describe( 'startWatching and stopWatching', () => {
Expand Down