Skip to content

Commit

Permalink
Node polyfills: Only apply them in Node.js, not in web browsers (#1089)
Browse files Browse the repository at this point in the history
Playground Block is broken in Safari because the Node polyfill library
kicks in the browser environment and sets up a polyfill that's
unsupported by Safari. This PR restricts those polyfills to just the
Node.js runtime.

## Testing Instructions

Confirm the CI checks pass
  • Loading branch information
adamziel authored Mar 6, 2024
1 parent 90f82ca commit 9ccab08
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 148 deletions.
307 changes: 160 additions & 147 deletions packages/php-wasm/node-polyfills/src/lib/blob.ts
Original file line number Diff line number Diff line change
@@ -1,166 +1,179 @@
/**
* WordPress Playground heavily realies on the File class. This module
* polyfill the File class for the different environments where
* WordPress Playground may run.
*/
if (typeof File === 'undefined') {
import { currentJsRuntime } from './current-js-runtime';

// Without this check, the polyfills below would also be applied
// in web browsers. Unfortunately, Safari doesn't sypport BYOB streams
// and doesn't support the polyfill provided here. Let's only apply
// those polyfills in Node.js environments.
if (currentJsRuntime === 'NODE') {
/**
* Polyfill the File class that isn't shipped in Node.js version 18.
*
* Blob conveniently provides a lot of the same methods as File, we
* just need to implement a few File-specific properties.
* WordPress Playground heavily realies on the File class. This module
* polyfill the File class for the different environments where
* WordPress Playground may run.
*/
class File extends Blob {
override readonly name;
readonly lastModified: number;
readonly lastModifiedDate: Date;
webkitRelativePath: any;
constructor(
sources: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(sources);
/*
* Compute a valid last modified date as that's what the
* browsers do:
*
* ```
* > new File([], '').lastModifiedDate
* Sat Dec 16 2023 10:07:53 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: NaN }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: 'string' }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: {} }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
* ```
*/
let date;
if (options?.lastModified) {
date = new Date();
}
if (!date || isNaN(date.getFullYear())) {
date = new Date();
if (typeof File === 'undefined') {
/**
* Polyfill the File class that isn't shipped in Node.js version 18.
*
* Blob conveniently provides a lot of the same methods as File, we
* just need to implement a few File-specific properties.
*/
class File extends Blob {
override readonly name;
readonly lastModified: number;
readonly lastModifiedDate: Date;
webkitRelativePath: any;
constructor(
sources: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(sources);
/*
* Compute a valid last modified date as that's what the
* browsers do:
*
* ```
* > new File([], '').lastModifiedDate
* Sat Dec 16 2023 10:07:53 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: NaN }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: 'string' }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
*
* > new File([], '', { lastModified: {} }).lastModifiedDate
* Thu Jan 01 1970 01:00:00 GMT+0100 (czas środkowoeuropejski standardowy)
* ```
*/
let date;
if (options?.lastModified) {
date = new Date();
}
if (!date || isNaN(date.getFullYear())) {
date = new Date();
}
this.lastModifiedDate = date;
this.lastModified = date.getMilliseconds();
this.name = fileName || '';
}
this.lastModifiedDate = date;
this.lastModified = date.getMilliseconds();
this.name = fileName || '';
}
global.File = File;
}
global.File = File;
}

function asPromise<T>(obj: FileReader) {
return new Promise<T>(function (resolve, reject) {
obj.onload = obj.onerror = function (event: Event) {
obj.onload = obj.onerror = null;
// eslint-disable-next-line no-inner-declarations
function asPromise<T>(obj: FileReader) {
return new Promise<T>(function (resolve, reject) {
obj.onload = obj.onerror = function (event: Event) {
obj.onload = obj.onerror = null;

if (event.type === 'load') {
resolve(obj.result as T);
} else {
reject(new Error('Failed to read the blob/file'));
}
};
});
}
if (event.type === 'load') {
resolve(obj.result as T);
} else {
reject(new Error('Failed to read the blob/file'));
}
};
});
}

/**
* File is a subclass of Blob. Let's polyfill the following Blob
* methods that are missing in JSDOM:
*
* – Blob.text()
* – Blob.stream()
* – Blob.arrayBuffer()
*
* See the related JSDom issue:
*
* – [Implement Blob.stream, Blob.text and Blob.arrayBuffer](https://github.com/jsdom/jsdom/issues/2555).
*
* @source `blob-polyfill` npm package.
* * By Eli Grey, https://eligrey.com
* * By Jimmy Wärting, https://github.com/jimmywarting
*/
if (typeof Blob.prototype.arrayBuffer === 'undefined') {
Blob.prototype.arrayBuffer = function arrayBuffer() {
const reader = new FileReader();
reader.readAsArrayBuffer(this);
return asPromise<Uint8Array>(reader);
};
}
/**
* File is a subclass of Blob. Let's polyfill the following Blob
* methods that are missing in JSDOM:
*
* – Blob.text()
* – Blob.stream()
* – Blob.arrayBuffer()
*
* See the related JSDom issue:
*
* – [Implement Blob.stream, Blob.text and Blob.arrayBuffer](https://github.com/jsdom/jsdom/issues/2555).
*
* @source `blob-polyfill` npm package.
* * By Eli Grey, https://eligrey.com
* * By Jimmy Wärting, https://github.com/jimmywarting
*/
if (typeof Blob.prototype.arrayBuffer === 'undefined') {
Blob.prototype.arrayBuffer = function arrayBuffer() {
const reader = new FileReader();
reader.readAsArrayBuffer(this);
return asPromise<Uint8Array>(reader);
};
}

if (typeof Blob.prototype.text === 'undefined') {
Blob.prototype.text = function text() {
const reader = new FileReader();
reader.readAsText(this);
return asPromise<string>(reader);
};
}
if (typeof Blob.prototype.text === 'undefined') {
Blob.prototype.text = function text() {
const reader = new FileReader();
reader.readAsText(this);
return asPromise<string>(reader);
};
}

/**
* Detects if BYOB (Bring Your Own Buffer) streams are supported
* in the current environment.
*
* BYOB is a new feature in the Streams API that allows reading
* an arbitrary number of bytes from a stream. It's not supported
* in older versions of Node.js.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader
*/
function isByobSupported() {
const inputBytes = new Uint8Array([1, 2, 3, 4]);
const file = new File([inputBytes], 'test');
const stream = file.stream();
try {
// This throws on older versions of node:
stream.getReader({ mode: 'byob' });
return true;
} catch (e) {
return false;
/**
* Detects if BYOB (Bring Your Own Buffer) streams are supported
* in the current environment.
*
* BYOB is a new feature in the Streams API that allows reading
* an arbitrary number of bytes from a stream. It's not supported
* in older versions of Node.js.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader
*/
// eslint-disable-next-line no-inner-declarations
function isByobSupported() {
const inputBytes = new Uint8Array([1, 2, 3, 4]);
const file = new File([inputBytes], 'test');
const stream = file.stream();
try {
// This throws on older versions of node:
stream.getReader({ mode: 'byob' });
return true;
} catch (e) {
return false;
}
}
}

/**
* Polyfill the stream() method if it either doesn't exist,
* or is an older version shipped with e.g. Node.js 18 where
* BYOB streams seem to be unsupported.
*/
if (typeof Blob.prototype.stream === 'undefined' || !isByobSupported()) {
Blob.prototype.stream = function () {
let position = 0;
// eslint-disable-next-line
const blob = this;
return new ReadableStream({
type: 'bytes',
// 0.5 MB seems like a reasonable chunk size, let's adjust
// this if needed.
autoAllocateChunkSize: 512 * 1024,
/**
* Polyfill the stream() method if it either doesn't exist,
* or is an older version shipped with e.g. Node.js 18 where
* BYOB streams seem to be unsupported.
*/
if (typeof Blob.prototype.stream === 'undefined' || !isByobSupported()) {
Blob.prototype.stream = function () {
let position = 0;
// eslint-disable-next-line
const blob = this;
return new ReadableStream({
type: 'bytes',
// 0.5 MB seems like a reasonable chunk size, let's adjust
// this if needed.
autoAllocateChunkSize: 512 * 1024,

async pull(controller) {
const view = controller.byobRequest!.view;
async pull(controller) {
const view = controller.byobRequest!.view;

// Read the next chunk of data:
const chunk = blob.slice(position, position + view!.byteLength);
const buffer = await chunk.arrayBuffer();
const uint8array = new Uint8Array(buffer);
// Read the next chunk of data:
const chunk = blob.slice(
position,
position + view!.byteLength
);
const buffer = await chunk.arrayBuffer();
const uint8array = new Uint8Array(buffer);

// Emit that chunk:
new Uint8Array(view!.buffer).set(uint8array);
const bytesRead = uint8array.byteLength;
controller.byobRequest!.respond(bytesRead);
// Emit that chunk:
new Uint8Array(view!.buffer).set(uint8array);
const bytesRead = uint8array.byteLength;
controller.byobRequest!.respond(bytesRead);

// Bump the position and close this stream once
// we've read the entire blob.
position += bytesRead;
if (position >= blob.size) {
controller.close();
}
},
});
};
// Bump the position and close this stream once
// we've read the entire blob.
position += bytesRead;
if (position >= blob.size) {
controller.close();
}
},
});
};
}
}

export default {};
16 changes: 16 additions & 0 deletions packages/php-wasm/node-polyfills/src/lib/current-js-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const currentJsRuntime = (function () {
if (typeof process !== 'undefined' && process.release?.name === 'node') {
return 'NODE';
} else if (typeof window !== 'undefined') {
return 'WEB';
} else if (
// @ts-ignore
typeof WorkerGlobalScope !== 'undefined' &&
// @ts-ignore
self instanceof (WorkerGlobalScope as any)
) {
return 'WORKER';
} else {
return 'NODE';
}
})();
4 changes: 3 additions & 1 deletion packages/php-wasm/node-polyfills/src/lib/custom-event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
if (typeof CustomEvent === 'undefined') {
import { currentJsRuntime } from './current-js-runtime';

if (currentJsRuntime === 'NODE' && typeof CustomEvent === 'undefined') {
class CustomEvent<T = any> extends Event {
readonly detail: T;
constructor(
Expand Down

0 comments on commit 9ccab08

Please sign in to comment.