Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
911b065
.
adamziel Sep 26, 2025
9e1336c
Basic terminal interaction!
adamziel Sep 26, 2025
2a1ea70
Run subprocesses and report any runtime errors
adamziel Sep 26, 2025
aa12750
small readability improvements
adamziel Sep 26, 2025
83144ea
Polyfill grapheme_strlen to prevent composer error
adamziel Sep 26, 2025
af571b5
React app php playground
adamziel Sep 27, 2025
39f0a2c
Prototype of file browser and terminal
adamziel Sep 27, 2025
56f65c1
Document follow-up work
adamziel Sep 27, 2025
3454b6e
PHP Playground UX improvements
adamziel Sep 28, 2025
10ff364
PHP Playground UX improvements – dont reload file picker tree, suppor…
adamziel Sep 28, 2025
93b6144
Display path of the edited file above the editor
adamziel Sep 28, 2025
59e377c
Document more todos
adamziel Sep 28, 2025
ad25ff8
Save&rerun whem cmd+s is pressed
adamziel Sep 28, 2025
f2d650c
Avoid filepickertree re-rendering. support cd command
adamziel Sep 28, 2025
76bd8c1
Add file and Add directory buttons
adamziel Sep 28, 2025
59de0bf
Contextual menu, rename, delete buttons
adamziel Sep 28, 2025
093741e
move most of the contextual menu logic to PlaygroundFilePicker
adamziel Sep 28, 2025
70e6333
reorganize the code
adamziel Sep 28, 2025
9b945b0
reorganize the code
adamziel Sep 28, 2025
b30e024
ux improvements around the file picker
adamziel Sep 28, 2025
7bb7f8e
ux improvements around the file picker
adamziel Sep 28, 2025
65953cb
ux improvements around the file picker
adamziel Sep 28, 2025
564742f
ux improvements around the file picker
adamziel Sep 28, 2025
96bda92
ux improvements around the file picker
adamziel Sep 28, 2025
f7e5acb
ux improvements around the file picker
adamziel Sep 28, 2025
36a9893
ux improvements around the file picker
adamziel Sep 28, 2025
44fa590
ux improvements around the file picker
adamziel Sep 28, 2025
1751588
ux improvements
adamziel Sep 28, 2025
7114664
Consistent cwd treatment across different terminal processes and edit…
adamziel Sep 29, 2025
a95b689
Simplify the logic in the file picker
adamziel Sep 29, 2025
c0500ed
Reorganize code for easier maintenance
adamziel Sep 29, 2025
80d1816
Wrap main terminal component in a placeholder loader
adamziel Sep 29, 2025
890e7e9
UX tweaks
adamziel Sep 29, 2025
ca67eae
Use WordPress dropdown menu component for context menu
adamziel Sep 29, 2025
f5d1f7f
UX improvements around the file epxlorer
adamziel Sep 29, 2025
15bbba4
Tests, cleanup code
adamziel Sep 29, 2025
db391d8
Refresh file pircker tree when a file is changed
adamziel Sep 29, 2025
82fc52f
simplifications, ux improvements
adamziel Sep 29, 2025
239a0bc
simplifications, ux improvements
adamziel Sep 29, 2025
80220c3
Merge two file tree components
adamziel Sep 30, 2025
974c268
Drag&drop interaction
adamziel Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"no-inner-declarations": 0,
"no-use-before-define": "off",
"react/prop-types": 0,
"react-hooks/exhaustive-deps": 0,
"no-console": 1,
"no-empty": 0,
"no-async-promise-executor": 0,
Expand Down
2,024 changes: 1,601 additions & 423 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@
},
"private": true,
"dependencies": {
"@codemirror/autocomplete": "6.19.0",
"@codemirror/commands": "6.8.1",
"@codemirror/lang-css": "6.3.1",
"@codemirror/lang-html": "6.4.10",
"@codemirror/lang-javascript": "6.2.4",
"@codemirror/lang-json": "6.0.2",
"@codemirror/lang-markdown": "6.3.4",
"@codemirror/lang-php": "6.0.2",
"@codemirror/language": "6.11.3",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.3",
"@playwright/experimental-ct-react": "1.55.1",
"@preact/signals-react": "1.3.6",
"@reduxjs/toolkit": "2.6.1",
"@types/xml2js": "0.4.14",
Expand All @@ -87,6 +100,7 @@
"react-dom": "18.3.1",
"react-hook-form": "7.53.0",
"react-redux": "8.1.3",
"react-resizable-panels": "3.0.6",
"react-transition-group": "4.4.5",
"sha.js": "2.4.11",
"tmp-promise": "3.0.3",
Expand Down Expand Up @@ -130,7 +144,7 @@
"@vitejs/plugin-react": "4.2.0",
"@wordpress/block-editor": "13.4.0",
"@wordpress/blocks": "13.4.0",
"@wordpress/components": "28.4.0",
"@wordpress/components": "30.4.0",
"@wordpress/core-data": "7.4.0",
"@wordpress/data": "10.4.0",
"@wordpress/element": "6.4.0",
Expand Down
116 changes: 111 additions & 5 deletions packages/php-wasm/universal/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,13 @@ function streamToPort(stream: ReadableStream<Uint8Array>): MessagePort {
}
} catch (e: any) {
try {
port1.postMessage({ t: 'error', m: e?.message || String(e) });
const serialized = serializeForPort(e);
port1.postMessage({
t: 'error',
e: serialized,
// Legacy field for backwards compatibility
m: (e as any)?.message || JSON.stringify(e),
});
} catch {
// Ignore error
}
Expand Down Expand Up @@ -407,10 +413,43 @@ function portToStream(port: MessagePort): ReadableStream<Uint8Array> {
controller.close();
cleanup();
break;
case 'error':
controller.error(new Error(data.m || 'Stream error'));
case 'error': {
if (data.e) {
let errorValue: unknown;
try {
errorValue = deserializeForPort(
data.e,
'MessagePort bridged stream error'
);
} catch (deserializationError: any) {
// Fallback: if deserialization fails, surface a generic error
errorValue = new Error(
deserializationError?.message ||
'Stream error'
);
}
controller.error(errorValue as any);
cleanup();
break;
}
// Legacy fallback using stringified message
let error = '';
try {
error = JSON.parse(data.m);
} catch {
// Ignore error
}
if (!error) {
error = data.m;
}
if (typeof error === 'string') {
controller.error(new Error(error));
} else {
controller.error(error);
}
cleanup();
break;
}
}
};
const cleanup = () => {
Expand Down Expand Up @@ -473,9 +512,12 @@ function promiseToPort(promise: Promise<any>): MessagePort {
})
.catch((err) => {
try {
const serialized = serializeForPort(err);
port1.postMessage({
t: 'reject',
m: (err as any)?.message || String(err),
e: serialized,
// Legacy field for backwards compatibility
m: (err as any)?.message || JSON.stringify(err),
});
} catch {
// Ignore error
Expand Down Expand Up @@ -505,7 +547,35 @@ function portToPromise(port: MessagePort): Promise<any> {
resolve(data.v);
} else if (data.t === 'reject') {
cleanup();
reject(new Error(data.m || ''));
if (data.e) {
try {
const errorValue = deserializeForPort(
data.e,
'MessagePort bridged promise rejected'
);
reject(errorValue as any);
} catch (deserializationError: any) {
reject(
new Error(
deserializationError?.message ||
'Promise rejected'
)
);
}
return;
}
// Legacy fallback using stringified message
let error = '';
try {
error = JSON.parse(data.m);
} catch {
// Ignore error
}
if (typeof error === 'string') {
reject(new Error(error));
} else {
reject(error);
}
}
};
const cleanup = () => {
Expand Down Expand Up @@ -599,6 +669,42 @@ const throwTransferHandlerCustom: Comlink.TransferHandler<

Comlink.transferHandlers.set('throw', throwTransferHandlerCustom);

// Utilities to serialize/deserialize thrown values over MessagePorts
function serializeForPort(value: unknown): SerializedError {
let serialized: SerializedError;
if (value instanceof Error) {
serialized = {
isError: true,
value: ErrorSerializer.serializeError(value),
};
// Preserve the original error class name
(serialized.value as any)['originalErrorClassName'] = (
value as Error
).constructor.name;
} else {
serialized = { isError: false, value };
}
return serialized;
}

function deserializeForPort(
serialized: SerializedError,
additionalMessage: string
): unknown {
if (serialized.isError) {
const error = ErrorSerializer.deserializeError(serialized.value);
// Chain host call stack at the bottom of the error chain
const additionalCallStack = new Error(additionalMessage);
let deepestError: any = error as any;
while (deepestError.cause) {
deepestError = deepestError.cause;
}
deepestError.cause = additionalCallStack;
return error;
}
return serialized.value;
}

function proxyClone(object: any): any {
return new Proxy(object, {
get(target, prop) {
Expand Down
7 changes: 5 additions & 2 deletions packages/php-wasm/universal/src/lib/fs-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,11 @@ export class FSHelpers {
* @param target
* @param link
*/
static symlink(FS: Emscripten.RootFS, target: string, link: string): any {
return FS.symlink(target, link);
static symlink(FS: Emscripten.RootFS, target: string, link: string): true {
// Will throw an error if the symlink cannot be created or
// return a FS Node if the symlink is created.
FS.symlink(target, link);
return true;
}

/**
Expand Down
47 changes: 39 additions & 8 deletions packages/php-wasm/universal/src/lib/php-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable {
absoluteUrl = '';
/** @inheritDoc @php-wasm/universal!RequestHandler.documentRoot */
documentRoot = '';
private chroot: string | null = null;

#eventListeners: Map<string, Set<PHPWorkerEventListener>> = new Map();

Expand Down Expand Up @@ -177,9 +178,7 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable {

/** @inheritDoc @php-wasm/universal!/PHP.run */
async run(request: PHPRunOptions): Promise<PHPResponse> {
const { php, reap } = await _private
.get(this)!
.requestHandler!.processManager.acquirePHPInstance();
const { php, reap } = await this.acquirePHPInstance();
try {
return await php.run(request);
} finally {
Expand All @@ -192,21 +191,48 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable {
argv: string[],
options?: { env?: Record<string, string> }
): Promise<StreamedPHPResponse> {
const { php, reap } = await _private
.get(this)!
.requestHandler!.processManager.acquirePHPInstance();
const { php, reap } = await this.acquirePHPInstance();
let response: StreamedPHPResponse;
try {
return await php.cli(argv, options);
} finally {
response = await php.cli(argv, options);
} catch (error) {
reap();
throw error;
}
/**
* Register the reap() callback to run asynchronously once
* the response is finished.
*
* We don't await for response.finished here. It is a
* `StreamedPHPResponse` instance and the caller may want
* to start processing the streamed data immediately.
*/
response.finished.finally(reap);
return response;
}

/** @inheritDoc @php-wasm/universal!/PHP.chdir */
chdir(path: string): void {
// Remember the new chroot for all PHP instances yet to be acquired.
this.chroot = path;
return _private.get(this)!.php!.chdir(path);
}

/** @inheritDoc @php-wasm/universal!/PHP.chdir */
cwd(): string {
return _private.get(this)!.php!.cwd();
}

private async acquirePHPInstance() {
const { php, reap } = await _private
.get(this)!
.requestHandler!.processManager.acquirePHPInstance();
if (this.chroot !== null) {
php.chdir(this.chroot);
}
return { php, reap };
}

/** @inheritDoc @php-wasm/universal!/PHP.setSapiName */
setSapiName(newName: string): void {
_private.get(this)!.php!.setSapiName(newName);
Expand Down Expand Up @@ -237,6 +263,11 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable {
return _private.get(this)!.php!.writeFile(path, data);
}

/** @inheritDoc @php-wasm/universal!/PHP.symlink */
symlink(target: string, path: string): void {
_private.get(this)!.php!.symlink(target, path);
}

/** @inheritDoc @php-wasm/universal!/PHP.unlink */
unlink(path: string): void {
return _private.get(this)!.php!.unlink(path);
Expand Down
Loading
Loading