Skip to content
Merged
32 changes: 20 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"@testing-library/react": "14.0.0",
"@tsconfig/docusaurus": "^1.0.5",
"@types/ajv": "1.0.0",
"@types/emscripten": "1.39.12",
"@types/file-saver": "^2.0.5",
"@types/jest": "^29.4.0",
"@types/node": "18.14.2",
Expand Down
21 changes: 20 additions & 1 deletion packages/php-wasm/node/src/test/php.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
loadPHPRuntime,
SupportedPHPVersions,
} from '@php-wasm/universal';
import { existsSync, rmSync, readFileSync } from 'fs';
import { existsSync, rmSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
import { createSpawnHandler, phpVar } from '@php-wasm/util';

const testDirPath = '/__test987654321';
Expand Down Expand Up @@ -818,6 +818,25 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => {
);
});

it('mv() from NODEFS to MEMFS should work', () => {
mkdirSync(__dirname + '/test-data/mount-contents/a/b', {
recursive: true,
});
writeFileSync(
__dirname + '/test-data/mount-contents/a/b/test.txt',
'contents'
);
php.mkdir('/nodefs');
php.mount(__dirname + '/test-data/mount-contents', '/nodefs');
php.mv('/nodefs/a', '/tmp/a');
expect(
existsSync(__dirname + '/test-data/mount-contents/a')
).toEqual(false);
expect(php.fileExists('/nodefs/a')).toEqual(false);
expect(php.fileExists('/tmp/a')).toEqual(true);
expect(php.readFileAsText('/tmp/a/b/test.txt')).toEqual('contents');
});

it('mkdir() should create a directory', () => {
php.mkdir(testDirPath);
expect(php.fileExists(testDirPath)).toEqual(true);
Expand Down
71 changes: 65 additions & 6 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="emscripten" />
import { PHPResponse } from './php-response';
import {
getEmscriptenFsError,
Expand All @@ -22,13 +23,30 @@ import {
improveWASMErrorReporting,
UnhandledRejectionsTarget,
} from './wasm-error-reporting';
import { Semaphore, createSpawnHandler, joinPaths } from '@php-wasm/util';
import {
Semaphore,
createSpawnHandler,
dirname,
joinPaths,
} from '@php-wasm/util';
import { PHPRequestHandler } from './php-request-handler';
import { logger } from '@php-wasm/logger';

const STRING = 'string';
const NUMBER = 'number';

// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Emscripten {
type NamespaceToInstance<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never;
};

export type FileSystemInstance = NamespaceToInstance<typeof FS> & {
mkdirTree(path: string): void;
lookupPath(path: string, opts?: any): FS.Lookup;
};
}

export const __private__dont__use = Symbol('__private__dont__use');

export class PHPExecutionFailureError extends Error {
Expand Down Expand Up @@ -743,8 +761,28 @@ export abstract class BasePHP implements IsomorphicLocalPHP, Disposable {

/** @inheritDoc */
mv(fromPath: string, toPath: string) {
const FS = this[__private__dont__use]
.FS as Emscripten.FileSystemInstance;

try {
this[__private__dont__use].FS.rename(fromPath, toPath);
// FS.rename moves the inode within the same filesystem.
// If fromPath and toPath are on different filesystems,
// the operation will fail. In that case, we need to do
// a recursive copy of all the files and remove the original.
// Note this is also what happens in the linux `mv` command.
const fromMount = FS.lookupPath(fromPath).node.mount;
const toMount = this.fileExists(toPath)
? FS.lookupPath(toPath).node.mount
: FS.lookupPath(dirname(toPath)).node.mount;
const movingBetweenFilesystems =
fromMount.mountpoint !== toMount.mountpoint;

if (movingBetweenFilesystems) {
copyRecursive(FS, fromPath, toPath);
this.rmdir(fromPath, { recursive: true });
} else {
FS.rename(fromPath, toPath);
}
} catch (e) {
const errmsg = getEmscriptenFsError(e);
if (!errmsg) {
Expand Down Expand Up @@ -904,8 +942,6 @@ export function normalizeHeaders(
return normalized;
}

type EmscriptenFS = any;

export function syncFSTo(
source: BasePHP,
target: BasePHP,
Expand All @@ -923,8 +959,8 @@ export function syncFSTo(
* Non-MEMFS nodes are ignored.
*/
export function copyFS(
source: EmscriptenFS,
target: EmscriptenFS,
source: Emscripten.FileSystemInstance,
target: Emscripten.FileSystemInstance,
path: string
) {
let oldNode;
Expand Down Expand Up @@ -967,3 +1003,26 @@ export function copyFS(
copyFS(source, target, joinPaths(path, filename));
}
}

function copyRecursive(
FS: Emscripten.FileSystemInstance,
fromPath: string,
toPath: string
) {
const fromNode = FS.lookupPath(fromPath).node;
if (FS.isDir(fromNode.mode)) {
FS.mkdirTree(toPath);
const filenames = FS.readdir(fromPath).filter(
(name: string) => name !== '.' && name !== '..'
);
for (const filename of filenames) {
copyRecursive(
FS,
joinPaths(fromPath, filename),
joinPaths(toPath, filename)
);
}
} else {
FS.writeFile(toPath, FS.readFile(fromPath));
}
}
2 changes: 1 addition & 1 deletion packages/php-wasm/universal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"types": ["vitest", "vite/client"]
"types": ["vitest", "vite/client", "emscripten"]
},
"files": [],
"include": [],
Expand Down
1 change: 1 addition & 0 deletions packages/playground/cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default defineConfig({
'@php-wasm/progress',
'@php-wasm/util',
'@wp-playground/wordpress',
'@wp-playground/common',
'@wp-playground/blueprints',
'yargs',
'express',
Expand Down