From c25ae223b1787642bc1be0c017a300c754530e16 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Sun, 17 Dec 2023 12:51:15 +0100 Subject: [PATCH 1/5] Polyfills for Node.js v18 and JSDOM runtimes --- package-lock.json | 42 +++-- .../php-wasm/node-polyfills/.eslintrc.json | 18 +++ packages/php-wasm/node-polyfills/README.md | 16 ++ packages/php-wasm/node-polyfills/package.json | 33 ++++ packages/php-wasm/node-polyfills/project.json | 55 +++++++ packages/php-wasm/node-polyfills/src/index.ts | 2 + .../node-polyfills/src/lib/blob.spec.ts | 57 +++++++ .../php-wasm/node-polyfills/src/lib/blob.ts | 143 ++++++++++++++++++ .../src/lib/custom-event.spec.ts | 18 +++ .../node-polyfills/src/lib/custom-event.ts | 33 ++++ .../php-wasm/node-polyfills/tsconfig.json | 22 +++ .../php-wasm/node-polyfills/tsconfig.lib.json | 10 ++ .../node-polyfills/tsconfig.spec.json | 25 +++ .../php-wasm/node-polyfills/vite.config.ts | 52 +++++++ .../node-polyfills/vitest-jsdom.config.ts | 10 ++ .../node-polyfills/vitest-node.config.ts | 10 ++ packages/php-wasm/node/src/index.ts | 3 + .../node/src/test/php-request-handler.spec.ts | 13 +- .../blueprints/src/lib/steps/run-sql.spec.ts | 42 +++-- .../blueprints/src/vitest-setup-file.ts | 2 + packages/playground/blueprints/vite.config.ts | 1 + tsconfig.base.json | 6 + 22 files changed, 568 insertions(+), 45 deletions(-) create mode 100644 packages/php-wasm/node-polyfills/.eslintrc.json create mode 100644 packages/php-wasm/node-polyfills/README.md create mode 100644 packages/php-wasm/node-polyfills/package.json create mode 100644 packages/php-wasm/node-polyfills/project.json create mode 100644 packages/php-wasm/node-polyfills/src/index.ts create mode 100644 packages/php-wasm/node-polyfills/src/lib/blob.spec.ts create mode 100644 packages/php-wasm/node-polyfills/src/lib/blob.ts create mode 100644 packages/php-wasm/node-polyfills/src/lib/custom-event.spec.ts create mode 100644 packages/php-wasm/node-polyfills/src/lib/custom-event.ts create mode 100644 packages/php-wasm/node-polyfills/tsconfig.json create mode 100644 packages/php-wasm/node-polyfills/tsconfig.lib.json create mode 100644 packages/php-wasm/node-polyfills/tsconfig.spec.json create mode 100644 packages/php-wasm/node-polyfills/vite.config.ts create mode 100644 packages/php-wasm/node-polyfills/vitest-jsdom.config.ts create mode 100644 packages/php-wasm/node-polyfills/vitest-node.config.ts create mode 100644 packages/playground/blueprints/src/vitest-setup-file.ts diff --git a/package-lock.json b/package-lock.json index 2a83b83b53f..d1274da4f06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13992,6 +13992,10 @@ "resolved": "packages/php-wasm/node", "link": true }, + "node_modules/@php-wasm/node-polyfills": { + "resolved": "packages/php-wasm/node-polyfills", + "link": true + }, "node_modules/@php-wasm/progress": { "resolved": "packages/php-wasm/progress", "link": true @@ -17837,6 +17841,10 @@ "resolved": "packages/playground/storage", "link": true }, + "node_modules/@wp-playground/stream-compression": { + "resolved": "packages/playground/stream-compression", + "link": true + }, "node_modules/@wp-playground/sync": { "resolved": "packages/playground/sync", "link": true @@ -44912,7 +44920,7 @@ "version": "0.1.5", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44921,7 +44929,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44930,16 +44938,20 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, + "packages/php-wasm/node-polyfills": { + "version": "0.0.1", + "license": "GPL-2.0-or-later" + }, "packages/php-wasm/progress": { "name": "@php-wasm/progress", "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44948,7 +44960,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44957,7 +44969,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44965,7 +44977,7 @@ "name": "@php-wasm/util", "version": "0.3.1", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44974,7 +44986,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44983,7 +44995,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -44991,7 +45003,7 @@ "name": "@wp-playground/blueprints", "version": "0.3.1", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -45000,7 +45012,7 @@ "version": "0.3.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -45009,7 +45021,7 @@ "version": "1.0.0", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } }, @@ -45027,6 +45039,10 @@ "version": "0.0.1", "license": "GPL-2.0-or-later" }, + "packages/playground/stream-compression": { + "version": "0.0.1", + "license": "GPL-2.0-or-later" + }, "packages/playground/sync": { "name": "@wp-playground/sync", "version": "0.0.1", @@ -45038,7 +45054,7 @@ "version": "0.0.1", "license": "GPL-2.0-or-later", "engines": { - "node": ">=16.15.1", + "node": ">=18.18.2", "npm": ">=8.11.0" } } diff --git a/packages/php-wasm/node-polyfills/.eslintrc.json b/packages/php-wasm/node-polyfills/.eslintrc.json new file mode 100644 index 00000000000..79fd7c1d982 --- /dev/null +++ b/packages/php-wasm/node-polyfills/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/php-wasm/node-polyfills/README.md b/packages/php-wasm/node-polyfills/README.md new file mode 100644 index 00000000000..cb7db2ece54 --- /dev/null +++ b/packages/php-wasm/node-polyfills/README.md @@ -0,0 +1,16 @@ +# php-wasm-node-polyfills + +Polyfills JavaScript classes and methods required by WordPress Playground. + +Ensures compatibility with the following environments: + +- Node.js >= 18 +- JSDom + +## Building + +Run `nx build php-wasm-node-polyfills` to build the library. + +## Running unit tests + +Run `nx test php-wasm-node-polyfills` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/php-wasm/node-polyfills/package.json b/packages/php-wasm/node-polyfills/package.json new file mode 100644 index 00000000000..b7cbb1fcd25 --- /dev/null +++ b/packages/php-wasm/node-polyfills/package.json @@ -0,0 +1,33 @@ +{ + "name": "@php-wasm/node-polyfills", + "version": "0.0.1", + "description": "PHP.wasm – polyfills for Node.js", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/wordpress-playground" + }, + "homepage": "https://developer.wordpress.org/playground", + "author": "The WordPress contributors", + "contributors": [ + { + "name": "Adam Zielinski", + "email": "adam@adamziel.com", + "url": "https://github.com/adamziel" + } + ], + "type": "module", + "main": "./index.cjs", + "module": "./index.js", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "directory": "../../../dist/packages/php-wasm/node-polyfills" + }, + "license": "GPL-2.0-or-later" +} diff --git a/packages/php-wasm/node-polyfills/project.json b/packages/php-wasm/node-polyfills/project.json new file mode 100644 index 00000000000..f9aaae277e4 --- /dev/null +++ b/packages/php-wasm/node-polyfills/project.json @@ -0,0 +1,55 @@ +{ + "name": "php-wasm-node-polyfills", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/php-wasm/node-polyfills/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/php-wasm/node-polyfills" + } + }, + "test": { + "executor": "nx:noop", + "dependsOn": ["test:vite:node", "test:vite:jsdom"] + }, + "test:esmcjs": { + "executor": "@wp-playground/nx-extensions:assert-built-esm-and-cjs", + "options": { + "outputPath": "dist/packages/php-wasm/node-polyfills" + }, + "dependsOn": ["build"] + }, + "test:vite:node": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "config": "packages/php-wasm/node-polyfills/vitest-node.config.ts", + "passWithNoTests": true, + "reportsDirectory": "../../../coverage/packages/php-wasm/node-polyfills" + } + }, + "test:vite:jsdom": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "config": "packages/php-wasm/node-polyfills/vitest-jsdom.config.ts", + "passWithNoTests": true, + "reportsDirectory": "../../../coverage/packages/php-wasm/node-polyfills" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "packages/php-wasm/node-polyfills/**/*.ts", + "packages/php-wasm/node-polyfills/package.json" + ] + } + } + }, + "tags": [] +} diff --git a/packages/php-wasm/node-polyfills/src/index.ts b/packages/php-wasm/node-polyfills/src/index.ts new file mode 100644 index 00000000000..2432ff82727 --- /dev/null +++ b/packages/php-wasm/node-polyfills/src/index.ts @@ -0,0 +1,2 @@ +import './lib/blob'; +import './lib/custom-event'; diff --git a/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts b/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts new file mode 100644 index 00000000000..489e9f868dd --- /dev/null +++ b/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts @@ -0,0 +1,57 @@ +import './blob'; + +describe('File class', () => { + it('Should exist', () => { + expect(File).not.toBe(undefined); + }); +}); + +describe('File.arrayBuffer() method', () => { + it('should exist', async () => { + expect(typeof File.prototype.arrayBuffer).toBe('function'); + }); + it('should resolve to a valid array buffer', async () => { + const inputBytes = new Uint8Array([1, 2, 3, 4]); + const file = new File([inputBytes], 'test'); + const outputBuffer = await file.arrayBuffer(); + const outputBytes = new Uint8Array(outputBuffer); + expect(outputBytes).toEqual(inputBytes); + }); +}); + +describe('File.stream() method', () => { + it('should exist', async () => { + expect(typeof File.prototype.stream).toBe('function'); + }); + it('should returns a valid stream', async () => { + const inputBytes = new Uint8Array([1, 2, 3, 4]); + const file = new File([inputBytes], 'test'); + const stream = file.stream(); + const reader = stream.getReader(); + + const firstRead = await reader.read(); + expect(firstRead.value).toEqual(inputBytes); + expect(firstRead.done).toBe(false); + + const secondRead = await reader.read(); + expect(secondRead.done).toBe(true); + }); + // + it.skip('should be a valid BYOB stream that allows reading an arbitrary number of bytes', async () => { + const inputBytes = new Uint8Array([1, 2, 3, 4]); + const file = new File([inputBytes], 'test'); + const stream = file.stream(); + const reader = stream.getReader({ mode: 'byob' }); + + const firstRead = await reader.read(new Uint8Array(3)); + expect(firstRead.value).toEqual(inputBytes.slice(0, 3)); + expect(firstRead.done).toBe(false); + + const secondRead = await reader.read(new Uint8Array(2)); + expect(secondRead.value).toEqual(inputBytes.slice(3)); + expect(secondRead.done).toBe(false); + + const thirdRead = await reader.read(new Uint8Array(2)); + expect(thirdRead.done).toBe(true); + }); +}); diff --git a/packages/php-wasm/node-polyfills/src/lib/blob.ts b/packages/php-wasm/node-polyfills/src/lib/blob.ts new file mode 100644 index 00000000000..ca3d0461be3 --- /dev/null +++ b/packages/php-wasm/node-polyfills/src/lib/blob.ts @@ -0,0 +1,143 @@ +/** + * 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') { + /** + * 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 || ''; + } + } + global.File = File; +} + +function asPromise(obj: FileReader) { + return new Promise(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')); + } + }; + }); +} + +/** + * 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(reader); + }; +} + +if (typeof Blob.prototype.text === 'undefined') { + Blob.prototype.text = function text() { + const reader = new FileReader(); + reader.readAsText(this); + return asPromise(reader); + }; +} + +/** + * 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') { + 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; + + // 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); + + // Bump the position and close this stream once + // we've read the entire blob. + position += bytesRead; + if (position >= blob.size) { + controller.close(); + } + }, + }); + }; +} + +export default {}; diff --git a/packages/php-wasm/node-polyfills/src/lib/custom-event.spec.ts b/packages/php-wasm/node-polyfills/src/lib/custom-event.spec.ts new file mode 100644 index 00000000000..7cdc263e91a --- /dev/null +++ b/packages/php-wasm/node-polyfills/src/lib/custom-event.spec.ts @@ -0,0 +1,18 @@ +import './custom-event'; + +describe('CustomEvent class', () => { + it('Should exist', () => { + expect(CustomEvent).not.toBe(undefined); + }); + it('Should be possible to construct', () => { + const instance = new CustomEvent('test', { + detail: { + custom: 'data', + }, + }); + expect(instance).not.toBe(undefined); + expect(instance.detail).toEqual({ + custom: 'data', + }); + }); +}); diff --git a/packages/php-wasm/node-polyfills/src/lib/custom-event.ts b/packages/php-wasm/node-polyfills/src/lib/custom-event.ts new file mode 100644 index 00000000000..cd4ca242aef --- /dev/null +++ b/packages/php-wasm/node-polyfills/src/lib/custom-event.ts @@ -0,0 +1,33 @@ +if (typeof CustomEvent === 'undefined') { + class CustomEvent extends Event { + readonly detail: T; + constructor( + name: string, + options: { + detail?: T; + bubbles?: boolean; + cancellable?: boolean; + composed?: boolean; + } = {} + ) { + super(name, options); + /* + * The bang symbol (`!`) here is a lie to make TypeScript happy. + * + * Without the bang TS has the following complaint: + * + * > T | undefined is not assignable to type T + * + * In reality, it's absolutely fine for T (or `options.detail`) + * to be undefined. However, the CustomEvent interface shipped + * with TypeScript doesn't think so and marks `this.details` as + * a required property. + * + * This little and harmless trick silences that error. + */ + this.detail = options.detail!; + } + initCustomEvent(): void {} + } + globalThis.CustomEvent = CustomEvent; +} diff --git a/packages/php-wasm/node-polyfills/tsconfig.json b/packages/php-wasm/node-polyfills/tsconfig.json new file mode 100644 index 00000000000..8f4a9ec329e --- /dev/null +++ b/packages/php-wasm/node-polyfills/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/php-wasm/node-polyfills/tsconfig.lib.json b/packages/php-wasm/node-polyfills/tsconfig.lib.json new file mode 100644 index 00000000000..672b0253c97 --- /dev/null +++ b/packages/php-wasm/node-polyfills/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/php-wasm/node-polyfills/tsconfig.spec.json b/packages/php-wasm/node-polyfills/tsconfig.spec.json new file mode 100644 index 00000000000..ee9bf732a8e --- /dev/null +++ b/packages/php-wasm/node-polyfills/tsconfig.spec.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/php-wasm/node-polyfills/vite.config.ts b/packages/php-wasm/node-polyfills/vite.config.ts new file mode 100644 index 00000000000..09a1407dc67 --- /dev/null +++ b/packages/php-wasm/node-polyfills/vite.config.ts @@ -0,0 +1,52 @@ +/// +import { join } from 'path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import viteTsConfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + cacheDir: '../../../node_modules/.vite/php-wasm-node-polyfills', + + plugins: [ + viteTsConfigPaths({ + root: '../../../', + }), + dts({ + entryRoot: 'src', + tsconfigPath: join(__dirname, 'tsconfig.lib.json'), + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'php-wasm-node-polyfills', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: [], + }, + }, + + test: { + globals: true, + cache: { + dir: '../../../node_modules/.vitest', + }, + environment: + 'Run this task with either "node" or "jsdom" configuration, e.g. nx run php-wasm-node-polyfills:test:node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, +}); diff --git a/packages/php-wasm/node-polyfills/vitest-jsdom.config.ts b/packages/php-wasm/node-polyfills/vitest-jsdom.config.ts new file mode 100644 index 00000000000..2000af8f987 --- /dev/null +++ b/packages/php-wasm/node-polyfills/vitest-jsdom.config.ts @@ -0,0 +1,10 @@ +/// +import config from './vite.config'; + +export default { + ...config, + test: { + ...config.test, + environment: 'jsdom', + }, +}; diff --git a/packages/php-wasm/node-polyfills/vitest-node.config.ts b/packages/php-wasm/node-polyfills/vitest-node.config.ts new file mode 100644 index 00000000000..160e1ff8626 --- /dev/null +++ b/packages/php-wasm/node-polyfills/vitest-node.config.ts @@ -0,0 +1,10 @@ +/// +import config from './vite.config'; + +export default { + ...config, + test: { + ...config.test, + environment: 'node', + }, +}; diff --git a/packages/php-wasm/node/src/index.ts b/packages/php-wasm/node/src/index.ts index f41a696fd20..f78770f427e 100644 --- a/packages/php-wasm/node/src/index.ts +++ b/packages/php-wasm/node/src/index.ts @@ -1 +1,4 @@ +// PHP.wasm requires WordPress Playground's Node polyfills. +import '@php-wasm/node-polyfills'; + export * from './lib'; diff --git a/packages/php-wasm/node/src/test/php-request-handler.spec.ts b/packages/php-wasm/node/src/test/php-request-handler.spec.ts index 4a9c05385c2..33c9281d02e 100644 --- a/packages/php-wasm/node/src/test/php-request-handler.spec.ts +++ b/packages/php-wasm/node/src/test/php-request-handler.spec.ts @@ -150,14 +150,11 @@ describe.each(SupportedPHPVersions)( url: '/index.php', method: 'POST', files: { - myFile: { - name: 'text.txt', - async arrayBuffer() { - return new TextEncoder().encode('Hello World') - .buffer; - }, - type: 'text/plain', - } as any, + myFile: new File( + [new TextEncoder().encode('Hello World').buffer], + 'text.txt', + { type: 'text/plain' } + ), }, headers: { 'Content-Type': 'multipart/form-data; boundary=boundary', diff --git a/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts b/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts index 50bf3eb6682..0bd38ed9c42 100644 --- a/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts +++ b/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts @@ -46,14 +46,10 @@ describe('Blueprint step runSql', () => { ); // Test a single query - const mockFileSingle = { - name: 'single-query.sql', - async arrayBuffer() { - return new TextEncoder().encode('SELECT * FROM wp_users;') - .buffer; - }, - type: 'text/plain', - } as any; + const mockFileSingle = new File( + [new TextEncoder().encode('SELECT * FROM wp_users;').buffer], + 'single-query.sql' + ); await runSql(php, { sql: mockFileSingle }); @@ -62,15 +58,14 @@ describe('Blueprint step runSql', () => { expect(singleQueryResult).toBe(singleQueryExpect); // Test a multiple queries - const mockFileMultiple = { - name: 'multiple-queries.sql', - async arrayBuffer() { - return new TextEncoder().encode( + const mockFileMultiple = new File( + [ + new TextEncoder().encode( `SELECT * FROM wp_users;\nSELECT * FROM wp_posts;\n` - ).buffer; - }, - type: 'text/plain', - } as any; + ).buffer, + ], + 'multiple-queries.sql' + ); await runSql(php, { sql: mockFileMultiple }); @@ -79,15 +74,14 @@ describe('Blueprint step runSql', () => { expect(multiQueryResult).toBe(multiQueryExpect); // Ensure it works the same if the last query is missing a trailing newline - const mockFileNoTrailingSpace = { - name: 'no-trailing-newline.sql', - async arrayBuffer() { - return new TextEncoder().encode( + const mockFileNoTrailingSpace = new File( + [ + new TextEncoder().encode( `SELECT * FROM wp_users;\nSELECT * FROM wp_posts;` - ).buffer; - }, - type: 'text/plain', - } as any; + ).buffer, + ], + 'no-trailing-newline.sql' + ); await runSql(php, { sql: mockFileNoTrailingSpace }); const noTrailingNewlineQueryResult = await php.readFileAsText( diff --git a/packages/playground/blueprints/src/vitest-setup-file.ts b/packages/playground/blueprints/src/vitest-setup-file.ts new file mode 100644 index 00000000000..77a9309bb7c --- /dev/null +++ b/packages/playground/blueprints/src/vitest-setup-file.ts @@ -0,0 +1,2 @@ +// PHP.wasm requires WordPress Playground's Node polyfills. +import '@php-wasm/node-polyfills'; diff --git a/packages/playground/blueprints/vite.config.ts b/packages/playground/blueprints/vite.config.ts index 00988ffb141..4c751ed872a 100644 --- a/packages/playground/blueprints/vite.config.ts +++ b/packages/playground/blueprints/vite.config.ts @@ -53,6 +53,7 @@ export default defineConfig({ cache: { dir: '../../../node_modules/.vitest', }, + setupFiles: ['./src/vitest-setup-file.ts'], environment: 'jsdom', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 1669feac586..174791bc8b1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,9 @@ "packages/php-wasm/fs-journal/src/index.ts" ], "@php-wasm/node": ["packages/php-wasm/node/src/index.ts"], + "@php-wasm/node-polyfills": [ + "packages/php-wasm/node-polyfills/src/index.ts" + ], "@php-wasm/private": ["packages/php-wasm/private/src/index.ts"], "@php-wasm/progress": ["packages/php-wasm/progress/src/index.ts"], "@php-wasm/scopes": ["packages/php-wasm/scopes/src/index.ts"], @@ -48,6 +51,9 @@ "@wp-playground/storage": [ "packages/playground/storage/src/index.ts" ], + "@wp-playground/stream-compression": [ + "packages/playground/stream-compression/src/index.ts" + ], "@wp-playground/sync": ["packages/playground/sync/src/index.ts"], "@wp-playground/unit-test-utils": [ "packages/playground/unit-test-utils/src/index.ts" From 26d105ed2ef27e0ce40f3b335922d9aa31b51067 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Sun, 17 Dec 2023 13:01:31 +0100 Subject: [PATCH 2/5] Remove stream-compression from tsconfig --- tsconfig.base.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index 174791bc8b1..cd45f56cb56 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -51,9 +51,6 @@ "@wp-playground/storage": [ "packages/playground/storage/src/index.ts" ], - "@wp-playground/stream-compression": [ - "packages/playground/stream-compression/src/index.ts" - ], "@wp-playground/sync": ["packages/playground/sync/src/index.ts"], "@wp-playground/unit-test-utils": [ "packages/playground/unit-test-utils/src/index.ts" From e70be53570e3f33a21b5c305a867e0570a687d12 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Sun, 17 Dec 2023 13:03:44 +0100 Subject: [PATCH 3/5] Restore BYOB stream polyifll --- .../node-polyfills/src/lib/blob.spec.ts | 3 +- .../php-wasm/node-polyfills/src/lib/blob.ts | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts b/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts index 489e9f868dd..7e19118e566 100644 --- a/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts +++ b/packages/php-wasm/node-polyfills/src/lib/blob.spec.ts @@ -36,8 +36,7 @@ describe('File.stream() method', () => { const secondRead = await reader.read(); expect(secondRead.done).toBe(true); }); - // - it.skip('should be a valid BYOB stream that allows reading an arbitrary number of bytes', async () => { + it('should be a valid BYOB stream that allows reading an arbitrary number of bytes', async () => { const inputBytes = new Uint8Array([1, 2, 3, 4]); const file = new File([inputBytes], 'test'); const stream = file.stream(); diff --git a/packages/php-wasm/node-polyfills/src/lib/blob.ts b/packages/php-wasm/node-polyfills/src/lib/blob.ts index ca3d0461be3..e6a24f2af6b 100644 --- a/packages/php-wasm/node-polyfills/src/lib/blob.ts +++ b/packages/php-wasm/node-polyfills/src/lib/blob.ts @@ -141,3 +141,56 @@ if (typeof Blob.prototype.stream === 'undefined') { } export default {}; + +async 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, + + 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); + + // 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(); + } + }, + }); + }; +} From e0620042569f8e1fb1c1d5783111148fdeffe354 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Sun, 17 Dec 2023 15:39:12 +0100 Subject: [PATCH 4/5] Fix isByobSupported() check --- .../php-wasm/node-polyfills/src/lib/blob.ts | 55 +++++-------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/packages/php-wasm/node-polyfills/src/lib/blob.ts b/packages/php-wasm/node-polyfills/src/lib/blob.ts index e6a24f2af6b..3ee87436ddf 100644 --- a/packages/php-wasm/node-polyfills/src/lib/blob.ts +++ b/packages/php-wasm/node-polyfills/src/lib/blob.ts @@ -101,53 +101,22 @@ if (typeof Blob.prototype.text === 'undefined') { } /** - * 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. + * 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 */ -if (typeof Blob.prototype.stream === 'undefined') { - 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; - - // 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); - - // Bump the position and close this stream once - // we've read the entire blob. - position += bytesRead; - if (position >= blob.size) { - controller.close(); - } - }, - }); - }; -} - -export default {}; - -async function isByobSupported() { +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' }); + // This throws on older versions of node: stream.getReader({ mode: 'byob' }); return true; } catch (e) { @@ -194,3 +163,5 @@ if (typeof Blob.prototype.stream === 'undefined' || !isByobSupported()) { }); }; } + +export default {}; From c5aa8eaadc4c3278c88d60725092725dc1e92238 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Sun, 17 Dec 2023 16:03:16 +0100 Subject: [PATCH 5/5] Don't use TextEncoder to create TextFiles --- .../node/src/test/php-request-handler.spec.ts | 8 +++----- .../blueprints/src/lib/steps/run-sql.spec.ts | 14 +++----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/php-wasm/node/src/test/php-request-handler.spec.ts b/packages/php-wasm/node/src/test/php-request-handler.spec.ts index 33c9281d02e..2d1a0a2699c 100644 --- a/packages/php-wasm/node/src/test/php-request-handler.spec.ts +++ b/packages/php-wasm/node/src/test/php-request-handler.spec.ts @@ -150,11 +150,9 @@ describe.each(SupportedPHPVersions)( url: '/index.php', method: 'POST', files: { - myFile: new File( - [new TextEncoder().encode('Hello World').buffer], - 'text.txt', - { type: 'text/plain' } - ), + myFile: new File(['Hello World'], 'text.txt', { + type: 'text/plain', + }), }, headers: { 'Content-Type': 'multipart/form-data; boundary=boundary', diff --git a/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts b/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts index 0bd38ed9c42..42313295e80 100644 --- a/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts +++ b/packages/playground/blueprints/src/lib/steps/run-sql.spec.ts @@ -47,7 +47,7 @@ describe('Blueprint step runSql', () => { // Test a single query const mockFileSingle = new File( - [new TextEncoder().encode('SELECT * FROM wp_users;').buffer], + ['SELECT * FROM wp_users;'], 'single-query.sql' ); @@ -59,11 +59,7 @@ describe('Blueprint step runSql', () => { // Test a multiple queries const mockFileMultiple = new File( - [ - new TextEncoder().encode( - `SELECT * FROM wp_users;\nSELECT * FROM wp_posts;\n` - ).buffer, - ], + [`SELECT * FROM wp_users;\nSELECT * FROM wp_posts;\n`], 'multiple-queries.sql' ); @@ -75,11 +71,7 @@ describe('Blueprint step runSql', () => { // Ensure it works the same if the last query is missing a trailing newline const mockFileNoTrailingSpace = new File( - [ - new TextEncoder().encode( - `SELECT * FROM wp_users;\nSELECT * FROM wp_posts;` - ).buffer, - ], + [`SELECT * FROM wp_users;\nSELECT * FROM wp_posts;`], 'no-trailing-newline.sql' );