diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000000..d90bf974a2
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1 @@
+preload = ["./packages/bun-extensions/src/import-files-as-text.ts"]
diff --git a/lerna.json b/lerna.json
index 6e47e6cd20..9d5c8688c2 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
- "version": "0.7.1",
+ "version": "0.7.2",
"useNx": true
}
diff --git a/package-lock.json b/package-lock.json
index 9283499f5c..d82a1b0fcd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17795,12 +17795,12 @@
"resolved": "packages/playground/blueprints",
"link": true
},
- "node_modules/@wp-playground/client": {
- "resolved": "packages/playground/client",
+ "node_modules/@wp-playground/cli": {
+ "resolved": "packages/playground/cli",
"link": true
},
- "node_modules/@wp-playground/node": {
- "resolved": "packages/playground/node",
+ "node_modules/@wp-playground/client": {
+ "resolved": "packages/playground/client",
"link": true
},
"node_modules/@wp-playground/nx-extensions": {
@@ -44913,7 +44913,7 @@
},
"packages/php-wasm/cli": {
"name": "@php-wasm/cli",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"bin": {
"cli": "php-wasm.js"
@@ -44930,7 +44930,7 @@
},
"packages/php-wasm/fs-journal": {
"name": "@php-wasm/fs-journal",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -44939,7 +44939,7 @@
},
"packages/php-wasm/logger": {
"name": "@php-wasm/logger",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -44948,7 +44948,7 @@
},
"packages/php-wasm/node": {
"name": "@php-wasm/node",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -44957,12 +44957,12 @@
},
"packages/php-wasm/node-polyfills": {
"name": "@php-wasm/node-polyfills",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later"
},
"packages/php-wasm/progress": {
"name": "@php-wasm/progress",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -44971,7 +44971,7 @@
},
"packages/php-wasm/scopes": {
"name": "@php-wasm/scopes",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=16.15.1",
@@ -44985,7 +44985,7 @@
},
"packages/php-wasm/universal": {
"name": "@php-wasm/universal",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -44994,7 +44994,7 @@
},
"packages/php-wasm/util": {
"name": "@php-wasm/util",
- "version": "0.7.1",
+ "version": "0.7.2",
"engines": {
"node": ">=18.18.0",
"npm": ">=8.11.0"
@@ -45002,7 +45002,7 @@
},
"packages/php-wasm/web": {
"name": "@php-wasm/web",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=16.15.1",
@@ -45011,7 +45011,7 @@
},
"packages/php-wasm/web-service-worker": {
"name": "@php-wasm/web-service-worker",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -45020,15 +45020,22 @@
},
"packages/playground/blueprints": {
"name": "@wp-playground/blueprints",
- "version": "0.7.1",
+ "version": "0.7.2",
"engines": {
"node": ">=18.18.0",
"npm": ">=8.11.0"
}
},
+ "packages/playground/cli": {
+ "version": "0.7.2",
+ "license": "GPL-2.0-or-later",
+ "bin": {
+ "cli": "wp-playground.js"
+ }
+ },
"packages/playground/client": {
"name": "@wp-playground/client",
- "version": "0.7.1",
+ "version": "0.7.2",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.18.0",
@@ -45037,7 +45044,8 @@
},
"packages/playground/node": {
"name": "@wp-playground/node",
- "version": "0.0.1"
+ "version": "0.0.1",
+ "extraneous": true
},
"packages/playground/remote": {
"name": "@wp-playground/remote",
diff --git a/packages/bun-extensions/src/import-files-as-text.ts b/packages/bun-extensions/src/import-files-as-text.ts
new file mode 100644
index 0000000000..d28a8b3cbc
--- /dev/null
+++ b/packages/bun-extensions/src/import-files-as-text.ts
@@ -0,0 +1,33 @@
+import { plugin, BunPlugin } from 'bun';
+
+/**
+ * Adds support for `import contents from "file.php?raw"`.
+ *
+ * It matches Vite's behavior, which is to read the file as text
+ * and return its textual content. This allows us to run Bun and
+ * Vite on the same codebase.
+ */
+export const ImportFilesAsTextPlugin: BunPlugin = {
+ name: 'ImportFilesAsTextPlugin',
+ setup(build) {
+ build.onLoad(
+ {
+ // Looks for paths with a `raw` query parameter.
+ filter: /[?&]raw(?:[&=]|$)/,
+ },
+ async (args) => {
+ const path = args.path.split('?')[0];
+ // Use Bun.file to read the .php file as text
+ const text = await Bun.file(path).text();
+
+ // Return the file content as a module exporting the text
+ return {
+ contents: `export default ${JSON.stringify(text)};`,
+ loader: 'js', // Treat the content as JavaScript
+ };
+ }
+ );
+ },
+};
+
+plugin(ImportFilesAsTextPlugin);
diff --git a/packages/php-wasm/cli/package.json b/packages/php-wasm/cli/package.json
index d52dd12e13..a5b6c65b33 100644
--- a/packages/php-wasm/cli/package.json
+++ b/packages/php-wasm/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/cli",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm CLI for node.js",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/compile/php/Dockerfile b/packages/php-wasm/compile/php/Dockerfile
index 63ee50f20f..e5a80081e9 100644
--- a/packages/php-wasm/compile/php/Dockerfile
+++ b/packages/php-wasm/compile/php/Dockerfile
@@ -1023,11 +1023,7 @@ RUN set -euxo pipefail; \
# Turn the php.js file into an ES module
# Manually turn the output into a esm module instead of relying on -s MODULARIZE=1.
# which pollutes the global namespace and does not play well with import() mechanics.
- if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
- echo "const dependencyFilename = __dirname + '/${PHP_VERSION_ESCAPED}/$WASM_FILENAME'; " >> /root/output/php-module.js; \
- else \
- echo "import dependencyFilename from './${PHP_VERSION_ESCAPED}/$WASM_FILENAME'; " >> /root/output/php-module.js; \
- fi; \
+ echo "import dependencyFilename from './${PHP_VERSION_ESCAPED}/$WASM_FILENAME'; " >> /root/output/php-module.js; \
echo "export { dependencyFilename }; " >> /root/output/php-module.js && \
echo "export const dependenciesTotalSize = $FILE_SIZE; " >> /root/output/php-module.js && \
cat /root/esm-prefix.js >> /root/output/php-module.js && \
diff --git a/packages/php-wasm/fs-journal/package.json b/packages/php-wasm/fs-journal/package.json
index fcc8306f18..0a545c8d04 100644
--- a/packages/php-wasm/fs-journal/package.json
+++ b/packages/php-wasm/fs-journal/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/fs-journal",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "Bindings to journal the PHP filesystem",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/fs-journal/vite.config.ts b/packages/php-wasm/fs-journal/vite.config.ts
index f5eb718ea6..def7c0b87d 100644
--- a/packages/php-wasm/fs-journal/vite.config.ts
+++ b/packages/php-wasm/fs-journal/vite.config.ts
@@ -7,6 +7,9 @@ import { join } from 'path';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { viteTsConfigPaths } from '../../vite-ts-config-paths';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { viteWasmLoader } from '../../vite-wasm-loader';
+
export default defineConfig({
cacheDir: '../../../node_modules/.vite/php-wasm-fs-journal',
@@ -19,6 +22,8 @@ export default defineConfig({
viteTsConfigPaths({
root: '../../../',
}),
+
+ viteWasmLoader,
],
// Configuration for building your library.
diff --git a/packages/php-wasm/logger/package.json b/packages/php-wasm/logger/package.json
index 21030b7118..8d44f95c3a 100644
--- a/packages/php-wasm/logger/package.json
+++ b/packages/php-wasm/logger/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/logger",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "A logger for PHP-wasm clients like Playground and WP-now.",
"repository": {
"type": "git",
@@ -19,7 +19,8 @@
"directory": "../../../dist/packages/php-wasm/logger"
},
"license": "GPL-2.0-or-later",
- "main": "index.cjs",
+ "type": "module",
+ "main": "index.js",
"types": "index.d.ts",
"engines": {
"node": ">=18.18.0",
diff --git a/packages/php-wasm/node-polyfills/package.json b/packages/php-wasm/node-polyfills/package.json
index fb7d693626..996851899a 100644
--- a/packages/php-wasm/node-polyfills/package.json
+++ b/packages/php-wasm/node-polyfills/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/node-polyfills",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm – polyfills for Node.js",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/node/bin/postprocess-php-modules.cjs b/packages/php-wasm/node/bin/postprocess-php-modules.cjs
new file mode 100644
index 0000000000..01a3ac61d5
--- /dev/null
+++ b/packages/php-wasm/node/bin/postprocess-php-modules.cjs
@@ -0,0 +1,37 @@
+/**
+ * Replaces `import dependencyFilename from ` with `const dependencyFilename = __dirName + `
+ * in all files matching glob `php_*.js` in directory specified by argv[0].
+ *
+ * We need this to:
+ *
+ * * Produce a CommonJS and ESM-compliant module using Vite
+ * * Make the uncompiled source compatible with Bun
+ */
+const fs = require('fs');
+const glob = require('glob');
+
+const directory = process.argv[2];
+
+const files = glob.globSync(`${directory}/php_*.js`);
+files.forEach((file) => {
+ fs.readFile(file, 'utf8', (err, data) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+
+ const updatedData = data.replace(
+ 'import dependencyFilename from ',
+ 'const dependencyFilename = __dirname + '
+ );
+
+ fs.writeFile(file, updatedData, 'utf8', (err) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+
+ console.log(`Updated file: ${file}`);
+ });
+ });
+});
diff --git a/packages/php-wasm/node/package.json b/packages/php-wasm/node/package.json
index 4aca891e64..4db602814c 100644
--- a/packages/php-wasm/node/package.json
+++ b/packages/php-wasm/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/node",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm for Node.js",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json
index 58a38431fb..c7be65c914 100644
--- a/packages/php-wasm/node/project.json
+++ b/packages/php-wasm/node/project.json
@@ -22,7 +22,9 @@
"executor": "nx:run-commands",
"options": {
"commands": [
- "cp -rf packages/php-wasm/node/public/* dist/packages/php-wasm/node"
+ "cp -rf packages/php-wasm/node/public/* dist/packages/php-wasm/node",
+ "node packages/php-wasm/node/bin/postprocess-php-modules.cjs dist/packages/php-wasm/node",
+ "rm -rf dist/packages/php-wasm/node/*.wasm"
],
"parallel": false
},
diff --git a/packages/php-wasm/node/public/php_7_0.js b/packages/php-wasm/node/public/php_7_0.js
index 73584e47ea..6cfc0811d1 100644
--- a/packages/php-wasm/node/public/php_7_0.js
+++ b/packages/php-wasm/node/public/php_7_0.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/7_0_33/php_7_0.wasm';
+import dependencyFilename from './7_0_33/php_7_0.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 13118939;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_7_1.js b/packages/php-wasm/node/public/php_7_1.js
index cfd5ddec91..2ca0ed923f 100644
--- a/packages/php-wasm/node/public/php_7_1.js
+++ b/packages/php-wasm/node/public/php_7_1.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/7_1_30/php_7_1.wasm';
+import dependencyFilename from './7_1_30/php_7_1.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 13643181;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_7_2.js b/packages/php-wasm/node/public/php_7_2.js
index 0740f1405d..5a981759ed 100644
--- a/packages/php-wasm/node/public/php_7_2.js
+++ b/packages/php-wasm/node/public/php_7_2.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/7_2_34/php_7_2.wasm';
+import dependencyFilename from './7_2_34/php_7_2.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 14335706;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_7_3.js b/packages/php-wasm/node/public/php_7_3.js
index 5557fbd3f8..f74a648f33 100644
--- a/packages/php-wasm/node/public/php_7_3.js
+++ b/packages/php-wasm/node/public/php_7_3.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/7_3_33/php_7_3.wasm';
+import dependencyFilename from './7_3_33/php_7_3.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 14441796;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_7_4.js b/packages/php-wasm/node/public/php_7_4.js
index 5fb0c418e4..612e569b56 100644
--- a/packages/php-wasm/node/public/php_7_4.js
+++ b/packages/php-wasm/node/public/php_7_4.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/7_4_33/php_7_4.wasm';
+import dependencyFilename from './7_4_33/php_7_4.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 14675209;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_8_0.js b/packages/php-wasm/node/public/php_8_0.js
index 3668c117aa..642efb0cce 100644
--- a/packages/php-wasm/node/public/php_8_0.js
+++ b/packages/php-wasm/node/public/php_8_0.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/8_0_30/php_8_0.wasm';
+import dependencyFilename from './8_0_30/php_8_0.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 13904218;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_8_1.js b/packages/php-wasm/node/public/php_8_1.js
index 32cf6ed804..951c909bcf 100644
--- a/packages/php-wasm/node/public/php_8_1.js
+++ b/packages/php-wasm/node/public/php_8_1.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/8_1_23/php_8_1.wasm';
+import dependencyFilename from './8_1_23/php_8_1.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 13884035;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_8_2.js b/packages/php-wasm/node/public/php_8_2.js
index 34450b6444..5cc193807b 100644
--- a/packages/php-wasm/node/public/php_8_2.js
+++ b/packages/php-wasm/node/public/php_8_2.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/8_2_10/php_8_2.wasm';
+import dependencyFilename from './8_2_10/php_8_2.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 14143340;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/public/php_8_3.js b/packages/php-wasm/node/public/php_8_3.js
index e476bf66cc..1018c3e77d 100644
--- a/packages/php-wasm/node/public/php_8_3.js
+++ b/packages/php-wasm/node/public/php_8_3.js
@@ -1,4 +1,4 @@
-const dependencyFilename = __dirname + '/8_3_0/php_8_3.wasm';
+import dependencyFilename from './8_3_0/php_8_3.wasm';
export { dependencyFilename };
export const dependenciesTotalSize = 14523141;
export function init(RuntimeName, PHPLoader) {
diff --git a/packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts b/packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts
index 4f7466b014..c8bb8129f3 100644
--- a/packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts
+++ b/packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts
@@ -118,7 +118,7 @@ export function initOutboundWebsocketProxyServer(
// Handle new WebSocket client
async function onWsConnect(client: any, request: http.IncomingMessage) {
- const clientAddr = client._socket.remoteAddress;
+ const clientAddr = client?._socket?.remoteAddress || client.url;
const clientLog = function (...args: any[]) {
log(' ' + clientAddr + ': ', ...args);
};
diff --git a/packages/php-wasm/node/src/lib/node-php.ts b/packages/php-wasm/node/src/lib/node-php.ts
index 61c44b31e8..fff26ccd9d 100644
--- a/packages/php-wasm/node/src/lib/node-php.ts
+++ b/packages/php-wasm/node/src/lib/node-php.ts
@@ -104,7 +104,12 @@ export class NodePHP extends BasePHP {
*/
@rethrowFileSystemError('Could not mount {path}')
mount(localPath: string | MountSettings, virtualFSPath: string) {
- if (!this.fileExists(virtualFSPath)) {
+ const localRoot =
+ typeof localPath === 'object' ? localPath.root : localPath;
+ if (
+ !this.fileExists(virtualFSPath) &&
+ lstatSync(localRoot).isDirectory()
+ ) {
this.mkdirTree(virtualFSPath);
}
this[__private__dont__use].FS.mount(
diff --git a/packages/php-wasm/node/vite.config.ts b/packages/php-wasm/node/vite.config.ts
index 8079730142..f665077f3d 100644
--- a/packages/php-wasm/node/vite.config.ts
+++ b/packages/php-wasm/node/vite.config.ts
@@ -5,8 +5,10 @@
///
import { defineConfig } from 'vite';
import viteTsConfigPaths from 'vite-tsconfig-paths';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { viteWasmLoader } from '../../vite-wasm-loader';
-export default defineConfig(() => {
+export default defineConfig(function () {
return {
cacheDir: '../../../node_modules/.vite/php-wasm',
@@ -14,14 +16,7 @@ export default defineConfig(() => {
viteTsConfigPaths({
root: '../../../',
}),
- {
- name: 'resolve-wasm-path',
- load(id): any {
- if (id.endsWith('.wasm')) {
- return `export default ${JSON.stringify(id)}`;
- }
- },
- },
+ viteWasmLoader,
],
// Configuration for building your library.
diff --git a/packages/php-wasm/progress/package.json b/packages/php-wasm/progress/package.json
index a78d1b7af2..8c42132fc3 100644
--- a/packages/php-wasm/progress/package.json
+++ b/packages/php-wasm/progress/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/progress",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm – loading progress monitoring",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts b/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts
index 4a57142d50..a398cdf477 100644
--- a/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts
+++ b/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts
@@ -68,6 +68,9 @@ export class EmscriptenDownloadMonitor extends EventTarget {
.pop()!;
if (!fileSize) {
fileSize = this.#assetsSizes[fileName];
+ } else if (!(fileName in this.#assetsSizes)) {
+ this.#assetsSizes[fileName] = fileSize;
+ this.#progress[fileName] = loaded;
}
if (!(fileName in this.#progress)) {
logger.warn(
diff --git a/packages/php-wasm/scopes/package.json b/packages/php-wasm/scopes/package.json
index 741ecdf5f2..d6df514939 100644
--- a/packages/php-wasm/scopes/package.json
+++ b/packages/php-wasm/scopes/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/scopes",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm – scoped URLs utils",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/universal/package.json b/packages/php-wasm/universal/package.json
index 23dfccaa3f..8ca662d44e 100644
--- a/packages/php-wasm/universal/package.json
+++ b/packages/php-wasm/universal/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/universal",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm – emscripten bindings for PHP",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/universal/src/lib/base-php.ts b/packages/php-wasm/universal/src/lib/base-php.ts
index f3b58bb67e..51d750fe75 100644
--- a/packages/php-wasm/universal/src/lib/base-php.ts
+++ b/packages/php-wasm/universal/src/lib/base-php.ts
@@ -225,7 +225,7 @@ export abstract class BasePHP implements IsomorphicLocalPHP, Disposable {
* @deprecated
*/
async request(request: PHPRequest): Promise {
- console.warn(
+ logger.warn(
'PHP.request() is deprecated. Please use new PHPRequestHandler() instead.'
);
if (!this.requestHandler) {
diff --git a/packages/php-wasm/util/package.json b/packages/php-wasm/util/package.json
index 0473b8a908..7c32ce84dc 100644
--- a/packages/php-wasm/util/package.json
+++ b/packages/php-wasm/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/util",
- "version": "0.7.1",
+ "version": "0.7.2",
"type": "commonjs",
"typedoc": {
"entryPoint": "./src/index.ts",
diff --git a/packages/php-wasm/web-service-worker/package.json b/packages/php-wasm/web-service-worker/package.json
index 96715ec31a..4776e0c5ff 100644
--- a/packages/php-wasm/web-service-worker/package.json
+++ b/packages/php-wasm/web-service-worker/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/web-service-worker",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm – service worker utils",
"repository": {
"type": "git",
diff --git a/packages/php-wasm/web/package.json b/packages/php-wasm/web/package.json
index ee495fdb9c..9103c86ba0 100644
--- a/packages/php-wasm/web/package.json
+++ b/packages/php-wasm/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@php-wasm/web",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "PHP.wasm for the web",
"repository": {
"type": "git",
diff --git a/packages/playground/blueprints/package.json b/packages/playground/blueprints/package.json
index 69ae175009..e5268d3859 100644
--- a/packages/playground/blueprints/package.json
+++ b/packages/playground/blueprints/package.json
@@ -1,6 +1,6 @@
{
"name": "@wp-playground/blueprints",
- "version": "0.7.1",
+ "version": "0.7.2",
"exports": {
".": {
"import": "./index.js",
diff --git a/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts b/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts
index 7f3c630343..f70f23ac2b 100644
--- a/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts
+++ b/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts
@@ -1,6 +1,90 @@
import { UniversalPHP } from '@php-wasm/universal';
-// @ts-ignore
-import zipFunctions from './zip-functions.php?raw';
+
+const zipFunctions = `open($output, ZipArchive::CREATE);
+ if ($res === TRUE) {
+ $directories = array(
+ $root . '/'
+ );
+ while (sizeof($directories)) {
+ $current_dir = array_pop($directories);
+
+ if ($handle = opendir($current_dir)) {
+ while (false !== ($entry = readdir($handle))) {
+ if ($entry == '.' || $entry == '..') {
+ continue;
+ }
+
+ $entry = join_paths($current_dir, $entry);
+ if (in_array($entry, $excludePaths)) {
+ continue;
+ }
+
+ if (is_dir($entry)) {
+ $directory_path = $entry . '/';
+ array_push($directories, $directory_path);
+ } else if (is_file($entry)) {
+ $zip->addFile($entry, substr($entry, strlen($zip_root)));
+ }
+ }
+ closedir($handle);
+ }
+ }
+ foreach ($additionalPaths as $disk_path => $zip_path) {
+ $zip->addFile($disk_path, $zip_path);
+ }
+ $zip->close();
+ chmod($output, 0777);
+ }
+}
+
+function join_paths()
+{
+ $paths = array();
+
+ foreach (func_get_args() as $arg) {
+ if ($arg !== '') {
+ $paths[] = $arg;
+ }
+ }
+
+ return preg_replace('#/+#', '/', join('/', $paths));
+}
+
+function unzip($zipPath, $extractTo, $overwrite = true)
+{
+ if (!is_dir($extractTo)) {
+ mkdir($extractTo, 0777, true);
+ }
+ $zip = new ZipArchive;
+ $res = $zip->open($zipPath);
+ if ($res === TRUE) {
+ $zip->extractTo($extractTo);
+ $zip->close();
+ chmod($extractTo, 0777);
+ }
+}
+
+
+function delTree($dir)
+{
+ $files = array_diff(scandir($dir), array('.', '..'));
+ foreach ($files as $file) {
+ (is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
+ }
+ return rmdir($dir);
+}
+`;
+
export async function runPhpWithZipFunctions(
playground: UniversalPHP,
code: string
diff --git a/packages/playground/blueprints/src/lib/utils/zip-functions.php b/packages/playground/blueprints/src/lib/utils/zip-functions.php
deleted file mode 100644
index a3fddee344..0000000000
--- a/packages/playground/blueprints/src/lib/utils/zip-functions.php
+++ /dev/null
@@ -1,83 +0,0 @@
-open($output, ZipArchive::CREATE);
- if ($res === TRUE) {
- $directories = array(
- $root . '/'
- );
- while (sizeof($directories)) {
- $current_dir = array_pop($directories);
-
- if ($handle = opendir($current_dir)) {
- while (false !== ($entry = readdir($handle))) {
- if ($entry == '.' || $entry == '..') {
- continue;
- }
-
- $entry = join_paths($current_dir, $entry);
- if (in_array($entry, $excludePaths)) {
- continue;
- }
-
- if (is_dir($entry)) {
- $directory_path = $entry . '/';
- array_push($directories, $directory_path);
- } else if (is_file($entry)) {
- $zip->addFile($entry, substr($entry, strlen($zip_root)));
- }
- }
- closedir($handle);
- }
- }
- foreach ($additionalPaths as $disk_path => $zip_path) {
- $zip->addFile($disk_path, $zip_path);
- }
- $zip->close();
- chmod($output, 0777);
- }
-}
-
-function join_paths()
-{
- $paths = array();
-
- foreach (func_get_args() as $arg) {
- if ($arg !== '') {
- $paths[] = $arg;
- }
- }
-
- return preg_replace('#/+#', '/', join('/', $paths));
-}
-
-function unzip($zipPath, $extractTo, $overwrite = true)
-{
- if (!is_dir($extractTo)) {
- mkdir($extractTo, 0777, true);
- }
- $zip = new ZipArchive;
- $res = $zip->open($zipPath);
- if ($res === TRUE) {
- $zip->extractTo($extractTo);
- $zip->close();
- chmod($extractTo, 0777);
- }
-}
-
-
-function delTree($dir)
-{
- $files = array_diff(scandir($dir), array('.', '..'));
- foreach ($files as $file) {
- (is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
- }
- return rmdir($dir);
-}
diff --git a/packages/playground/blueprints/vite.config.ts b/packages/playground/blueprints/vite.config.ts
index d5912f3a93..c1f407c2d1 100644
--- a/packages/playground/blueprints/vite.config.ts
+++ b/packages/playground/blueprints/vite.config.ts
@@ -6,6 +6,8 @@ import { join } from 'path';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { viteTsConfigPaths } from '../../vite-ts-config-paths';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { viteWasmLoader } from '../../vite-wasm-loader';
export default defineConfig({
cacheDir: '../../../node_modules/.vite/playground-blueprints',
@@ -19,6 +21,8 @@ export default defineConfig({
viteTsConfigPaths({
root: '../../../',
}),
+
+ viteWasmLoader,
],
// Uncomment this if you are using workers.
diff --git a/packages/playground/cli/.eslintrc.json b/packages/playground/cli/.eslintrc.json
new file mode 100644
index 0000000000..79fd7c1d98
--- /dev/null
+++ b/packages/playground/cli/.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/playground/cli/README.md b/packages/playground/cli/README.md
new file mode 100644
index 0000000000..5619e35b0a
--- /dev/null
+++ b/packages/playground/cli/README.md
@@ -0,0 +1,76 @@
+# WordPress Playground CLI
+
+A CLI tool for running WordPress playground locally instead of in the browser:
+
+```shell
+$ bun packages/playground/cli/src/cli.ts start --wp=6.5
+WordPress is running on http://127.0.0.1:9400
+```
+
+Playground CLI is simple, configurable, and unoppinionated. You can set it up
+to your unique WordPress setup. For example, this command would run the documentation
+workflow at https://github.com/adamziel/playground-docs-workflow:
+
+```shell
+bun --config=/Users/adam/.bunfig.toml \
+ ./packages/playground/cli/src/cli.ts \
+ start \
+ --mount=./wp-content/plugins/wp-docs-plugin:/wordpress/wp-content/plugins/wp-docs-plugin \
+ --mount=./wp-content/html-pages:/wordpress/wp-content/html-pages \
+ --mount=./wp-content/uploads:/wordpress/wp-content/uploads \
+ --mount=./wp-content/themes/playground-docs:/wordpress/wp-content/themes/playground-docs \
+ --blueprint=./wp-content/blueprint-wp-now.json \
+ --wp=6.5
+```
+
+It is long, sure, but it is also very flexible. If you need a shorter version, you can alias
+it or write a bash script. In the future, Blueprints might support relative path mappings,
+at which point that command would get much shorter.
+
+## Philosophy
+
+The data flow is as follows:
+
+- Start PHP
+- Mount any local directories
+- Put a fresh WordPress in the resulting virtual filesystem (unless you're mounting directly at /wordpress).
+- Run the Blueprint
+- Start a local server, accept requests
+
+On each run, a fresh WordPress release is unzipped in the virtual filesystem. It is sourced
+from a zip file cached at ~/.wordpress-playground/. If you mess up your site, just restart the
+server and you'll get a fresh one, again unzipped. The CLI tool never modifies the zip file
+so you can always be sure you're starting from a clean slate.
+
+## Future work
+
+The CLI tool will have the following commands:
+
+- `serve` - start a fresh WordPress playground server.
+- `build` – run a Blueprint and output a .zip file with the resulting WordPress instance.
+- `php` - run the specified PHP file.
+
+It will also support:
+
+- Loading Blueprints from URLs.
+- Saving the running WordPress site and loading it later.
+- Caching all remote resources referenced in Blueprints. Currently, say, plugins are downloaded on each run.
+
+Conceptually, this isn't too different from Docker containers. There are images (zip files),
+containers (running instances), and commands (Blueprints). Playground could support the same
+concepts such as:
+
+- Listing and managing available images and containers.
+- Saving a running container and restoring it later
+- Starting a container from a specific image (already supported via zip files)
+- Running a command in a container (the `php` command)
+- Building a new image from a Blueprint (the `build` command)
+- Step-by-step cache for Blueprints so that only the changed steps are re-run.
+
+## Interoperability
+
+This CLI package is not just a useful tool. It drives interoperability between the in-browser
+Playground, CLI packages, and the PHP Blueprints library. Once complete, it will reuse the
+same internals as the website at https://playground.wordpress.org whether we're talking about
+running PHP code, executing Blueprints, building snapshots, serving requests, or maintaining
+multiple PHP instances
diff --git a/packages/playground/cli/jest.config.ts b/packages/playground/cli/jest.config.ts
new file mode 100644
index 0000000000..1fb6ef5671
--- /dev/null
+++ b/packages/playground/cli/jest.config.ts
@@ -0,0 +1,13 @@
+/* eslint-disable */
+export default {
+ displayName: 'nx-extensions',
+ preset: '../../../jest.preset.js',
+ transform: {
+ '^.+\\.[tj]s$': [
+ 'ts-jest',
+ { tsconfig: '/tsconfig.spec.json' },
+ ],
+ },
+ moduleFileExtensions: ['ts', 'js', 'html'],
+ coverageDirectory: '../../../coverage/packages/nx-extensions',
+};
diff --git a/packages/playground/cli/package.json b/packages/playground/cli/package.json
new file mode 100644
index 0000000000..9ad5deea20
--- /dev/null
+++ b/packages/playground/cli/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@wp-playground/cli",
+ "version": "0.7.2",
+ "description": "WordPress Playground CLI",
+ "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"
+ }
+ ],
+ "publishConfig": {
+ "access": "public",
+ "directory": "../../../dist/packages/playground/cli"
+ },
+ "license": "GPL-2.0-or-later",
+ "type": "module",
+ "main": "main.js",
+ "bin": "wp-playground.js",
+ "gitHead": "2f8d8f3cea548fbd75111e8659a92f601cddc593"
+}
diff --git a/packages/playground/cli/project.json b/packages/playground/cli/project.json
new file mode 100644
index 0000000000..2c860bfe59
--- /dev/null
+++ b/packages/playground/cli/project.json
@@ -0,0 +1,84 @@
+{
+ "name": "wp-playground-cli",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/playground/cli/src",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@wp-playground/nx-extensions:package-json",
+ "options": {
+ "tsConfig": "packages/playground/cli/tsconfig.lib.json",
+ "outputPath": "dist/packages/playground/cli",
+ "buildTarget": "wp-playground-cli:build:bundle:production"
+ }
+ },
+ "build:bundle": {
+ "executor": "@nx/vite:build",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "main": "dist/packages/playground/cli/src/cli.js",
+ "outputPath": "dist/packages/playground/cli"
+ },
+ "defaultConfiguration": "production",
+ "configurations": {
+ "development": {
+ "minify": false
+ },
+ "production": {
+ "minify": true
+ }
+ }
+ },
+ "dev": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "bun --watch ./packages/playground/cli/src/cli.ts"
+ }
+ },
+ "start": {
+ "executor": "@wp-playground/nx-extensions:built-script",
+ "options": {
+ "scriptPath": "dist/packages/playground/cli/wp-playground.js"
+ },
+ "dependsOn": ["build"]
+ },
+ "publish": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "node tools/scripts/publish.mjs php-wasm-cli {args.ver} {args.tag}"
+ },
+ "dependsOn": ["build"]
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["packages/playground/cli/**/*.ts"]
+ }
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "packages/playground/cli/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "typecheck": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": [
+ "tsc -p packages/playground/cli/tsconfig.lib.json --noEmit",
+ "tsc -p packages/playground/cli/tsconfig.spec.json --noEmit"
+ ]
+ }
+ }
+ },
+ "tags": ["scope:php-wasm-public"]
+}
diff --git a/packages/playground/cli/public/wp-playground.js b/packages/playground/cli/public/wp-playground.js
new file mode 100644
index 0000000000..cf80c624bb
--- /dev/null
+++ b/packages/playground/cli/public/wp-playground.js
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+import './cli.js';
diff --git a/packages/playground/cli/src/cli.ts b/packages/playground/cli/src/cli.ts
new file mode 100644
index 0000000000..714f792a92
--- /dev/null
+++ b/packages/playground/cli/src/cli.ts
@@ -0,0 +1,315 @@
+import fs from 'fs';
+import path from 'path';
+import yargs from 'yargs';
+import { startServer } from './server';
+import {
+ PHPRequest,
+ PHPRequestHandler,
+ PHPResponse,
+ SupportedPHPVersion,
+ SupportedPHPVersions,
+} from '@php-wasm/universal';
+import { logger } from '@php-wasm/logger';
+import { createPhp } from './setup-php';
+import { setupWordPress } from './setup-wp';
+import {
+ Blueprint,
+ compileBlueprint,
+ defineSiteUrl,
+ runBlueprintSteps,
+} from '@wp-playground/blueprints';
+import { RecommendedPHPVersion } from '@wp-playground/wordpress';
+import { NodePHP } from '@php-wasm/node';
+import { isValidWordPressSlug } from './is-valid-wordpress-slug';
+import { EmscriptenDownloadMonitor, ProgressTracker } from '@php-wasm/progress';
+
+export interface Mount {
+ hostPath: string;
+ vfsPath: string;
+}
+
+async function run() {
+ /**
+ * @TODO This looks similar to Query API args https://wordpress.github.io/wordpress-playground/query-api
+ * Perhaps the two could be handled by the same code?
+ */
+ const yargsObject = await yargs(process.argv.slice(2))
+ .usage('Usage: wp-playground [options]')
+ .positional('command', {
+ describe: 'Command to run',
+ type: 'string',
+ choices: ['server', 'run-blueprint', 'build-snapshot'],
+ })
+ .option('outfile', {
+ describe: 'When building, write to this output file.',
+ type: 'string',
+ default: 'wordpress.zip',
+ })
+ .option('port', {
+ describe: 'Port to listen on when serving.',
+ type: 'number',
+ default: 9400,
+ })
+ .option('php', {
+ describe: 'PHP version to use.',
+ type: 'string',
+ default: RecommendedPHPVersion,
+ choices: SupportedPHPVersions,
+ })
+ .option('wp', {
+ describe: 'WordPress version to use.',
+ type: 'string',
+ default: 'latest',
+ })
+ // @TODO: Support read-only mounts, e.g. via WORKERFS, a custom ReadOnlyNODEFS, or by copying the files into MEMFS
+ .option('mount', {
+ describe:
+ 'Mount a directory to the PHP runtime. You can provide --mount multiple times. Format: /host/path:/vfs/path',
+ type: 'array',
+ string: true,
+ })
+ .option('login', {
+ describe: 'Should log the user in',
+ type: 'boolean',
+ default: false,
+ })
+ .option('blueprint', {
+ describe: 'Blueprint to execute.',
+ type: 'string',
+ })
+ .option('skipWordPressSetup', {
+ describe:
+ 'Do not download, unzip, and install WordPress. Useful for mounting a pre-configured WordPress directory at /wordpress.',
+ type: 'boolean',
+ default: false,
+ })
+ .option('quiet', {
+ describe: 'Do not output logs and progress messages.',
+ type: 'boolean',
+ default: false,
+ })
+ .check((args) => {
+ if (args.wp !== undefined && !isValidWordPressSlug(args.wp)) {
+ throw new Error(
+ 'Unrecognized WordPress version. Please use "latest" or numeric versions such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"'
+ );
+ }
+ if (args.blueprint !== undefined) {
+ const blueprintPath = path.resolve(
+ process.cwd(),
+ args.blueprint
+ );
+ if (!fs.existsSync(blueprintPath)) {
+ throw new Error('Blueprint file does not exist');
+ }
+
+ const content = fs.readFileSync(blueprintPath, 'utf-8');
+ try {
+ args.blueprint = JSON.parse(content);
+ } catch (e) {
+ throw new Error('Blueprint file is not a valid JSON file');
+ }
+ }
+ return true;
+ });
+
+ yargsObject.wrap(yargsObject.terminalWidth());
+ const args = await yargsObject.argv;
+
+ if (args.quiet) {
+ // @ts-ignore
+ logger.handlers = [];
+ }
+
+ /**
+ * TODO: This exact feature will be provided in the PHP Blueprints library.
+ * Let's use it when it ships. Let's also use it in the web Playground app.
+ */
+ async function zipSite(outfile: string) {
+ // Fake URL for the build
+ const { php, reap } =
+ await requestHandler.processManager.acquirePHPInstance();
+ try {
+ await php.run({
+ code: `open('/tmp/build.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
+ throw new Exception('Failed to create ZIP');
+ }
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator('/wordpress')
+ );
+ foreach ($files as $file) {
+ echo $file . PHP_EOL;
+ if (!$file->isFile()) {
+ continue;
+ }
+ $zip->addFile($file->getPathname(), $file->getPathname());
+ }
+ $zip->close();
+
+ `,
+ });
+ const zip = php.readFileAsBuffer('/tmp/build.zip');
+ fs.writeFileSync(outfile, zip);
+ } finally {
+ reap();
+ }
+ }
+
+ async function prepareSite(
+ php: NodePHP,
+ wpVersion: string,
+ siteUrl: string
+ ) {
+ // No need to unzip WordPress if it's already mounted at /wordpress
+ if (!args.skipWordPressSetup) {
+ logger.log(`Setting up WordPress ${wpVersion}`);
+ // @TODO: Rename to FetchProgressMonitor. There's nothing Emscripten about that class anymore.
+ const monitor = new EmscriptenDownloadMonitor();
+ monitor.addEventListener('progress', ((
+ e: CustomEvent
+ ) => {
+ // @TODO Every progres bar will want percentages. The
+ // download monitor should just provide that.
+ const percentProgress = Math.round(
+ Math.min(100, (100 * e.detail.loaded) / e.detail.total)
+ );
+ if (!args.quiet) {
+ process.stdout.write(
+ `\rDownloading WordPress ${percentProgress}%... `
+ );
+ }
+ }) as any);
+ await setupWordPress(php, wpVersion, monitor);
+ }
+
+ const mounts: Mount[] = (args.mount || []).map((mount) => {
+ const [source, vfsPath] = mount.split(':');
+ return {
+ hostPath: path.resolve(process.cwd(), source),
+ vfsPath,
+ };
+ });
+ for (const mount of mounts) {
+ php.mount(mount.hostPath, mount.vfsPath);
+ }
+
+ await defineSiteUrl(php, {
+ siteUrl,
+ });
+ }
+
+ function compileInputBlueprint() {
+ /**
+ * @TODO This looks similar to the resolveBlueprint() call in the website package:
+ * https://github.com/WordPress/wordpress-playground/blob/ce586059e5885d185376184fdd2f52335cca32b0/packages/playground/website/src/main.tsx#L41
+ *
+ * Also the Blueprint Builder tool does something similar.
+ * Perhaps all these cases could be handled by the same function?
+ */
+ let blueprint: Blueprint | undefined;
+ if (args.blueprint) {
+ blueprint = args.blueprint as Blueprint;
+ } else {
+ blueprint = {
+ preferredVersions: {
+ php: args.php as SupportedPHPVersion,
+ wp: args.wp,
+ },
+ login: args.login,
+ };
+ }
+
+ const tracker = new ProgressTracker();
+ let lastCaption = '';
+ let progress100 = false;
+ tracker.addEventListener('progress', (e: any) => {
+ if (progress100) {
+ return;
+ } else if (e.detail.progress === 100) {
+ progress100 = true;
+ }
+ lastCaption =
+ e.detail.caption || lastCaption || 'Running the Blueprint';
+ process.stdout.write(
+ '\r\x1b[K' + `${lastCaption.trim()} – ${e.detail.progress}%`
+ );
+ if (progress100) {
+ process.stdout.write('\n');
+ }
+ });
+ return compileBlueprint(blueprint as Blueprint, {
+ progress: tracker,
+ });
+ }
+
+ const command = args._[0] as string;
+ if (!['run-blueprint', 'server', 'build-snapshot'].includes(command)) {
+ yargsObject.showHelp();
+ process.exit(1);
+ }
+
+ const compiledBlueprint = compileInputBlueprint();
+
+ let requestHandler: PHPRequestHandler;
+ let wordPressReady = false;
+
+ logger.log('Starting a PHP server...');
+
+ startServer({
+ port: args['port'] as number,
+ onBind: async (port: number) => {
+ const absoluteUrl = `http://127.0.0.1:${port}`;
+ requestHandler = new PHPRequestHandler({
+ phpFactory: async ({ isPrimary }) =>
+ createPhp(
+ requestHandler,
+ compiledBlueprint.versions.php,
+ isPrimary
+ ),
+ documentRoot: '/wordpress',
+ absoluteUrl,
+ });
+
+ const php = await requestHandler.getPrimaryPhp();
+ await prepareSite(php, compiledBlueprint.versions.wp, absoluteUrl);
+
+ wordPressReady = true;
+
+ if (compiledBlueprint) {
+ const { php, reap } =
+ await requestHandler.processManager.acquirePHPInstance();
+ try {
+ logger.log(`Running the Blueprint...`);
+ await runBlueprintSteps(compiledBlueprint, php);
+ logger.log(`Finished running the blueprint`);
+ } finally {
+ reap();
+ }
+ }
+
+ if (command === 'build-snapshot') {
+ zipSite(args.outfile as string);
+ logger.log(`WordPress exported to ${args.outfile}`);
+ process.exit(0);
+ } else if (command === 'run-blueprint') {
+ logger.log(`Blueprint executed`);
+ process.exit(0);
+ } else {
+ logger.log(`WordPress is running on ${absoluteUrl}`);
+ }
+ },
+ async handleRequest(request: PHPRequest) {
+ if (!wordPressReady) {
+ return PHPResponse.forHttpCode(
+ 502,
+ 'WordPress is not ready yet'
+ );
+ }
+ return await requestHandler.request(request);
+ },
+ });
+}
+
+run();
diff --git a/packages/playground/cli/src/download.ts b/packages/playground/cli/src/download.ts
new file mode 100644
index 0000000000..31bad56a8b
--- /dev/null
+++ b/packages/playground/cli/src/download.ts
@@ -0,0 +1,84 @@
+import { EmscriptenDownloadMonitor } from '@php-wasm/progress';
+import fs from 'fs-extra';
+import os from 'os';
+import path, { basename } from 'path';
+
+export const CACHE_FOLDER = path.join(os.homedir(), '.wordpress-playground');
+
+// @TODO: Support HTTP cache, invalidate the local file if the remote file has changed
+export async function cachedDownload(
+ remoteUrl: string,
+ cacheKey: string,
+ monitor: EmscriptenDownloadMonitor
+) {
+ const artifactPath = path.join(CACHE_FOLDER, cacheKey);
+ if (!fs.existsSync(artifactPath)) {
+ fs.ensureDirSync(CACHE_FOLDER);
+ await downloadTo(remoteUrl, artifactPath, monitor);
+ }
+ return readAsFile(artifactPath);
+}
+
+async function downloadTo(
+ remoteUrl: string,
+ localPath: string,
+ monitor: EmscriptenDownloadMonitor
+) {
+ const response = await monitor.monitorFetch(fetch(remoteUrl));
+ const reader = response.body!.getReader();
+ const writer = fs.createWriteStream(localPath);
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
+ writer.write(Buffer.from(value));
+ }
+}
+
+export function readAsFile(path: string, fileName?: string): File {
+ return new File([fs.readFileSync(path)], fileName ?? basename(path));
+}
+
+export async function resolveWPRelease(version = 'latest') {
+ if (version === 'trunk' || version === 'nightly') {
+ return {
+ url: 'https://wordpress.org/nightly-builds/wordpress-latest.zip',
+ version: 'nightly-' + new Date().toISOString().split('T')[0],
+ };
+ }
+
+ let latestVersions = await fetch(
+ 'https://api.wordpress.org/core/version-check/1.7/?channel=beta'
+ ).then((res) => res.json());
+
+ latestVersions = latestVersions.offers.filter(
+ (v: any) => v.response === 'autoupdate'
+ );
+
+ for (const apiVersion of latestVersions) {
+ if (version === 'beta' && apiVersion.version.includes('beta')) {
+ return {
+ url: apiVersion.download,
+ version: apiVersion.version,
+ };
+ } else if (version === 'latest') {
+ return {
+ url: apiVersion.download,
+ version: apiVersion.version,
+ };
+ } else if (
+ apiVersion.version.substring(0, version.length) === version
+ ) {
+ return {
+ url: apiVersion.download,
+ version: apiVersion.version,
+ };
+ }
+ }
+
+ return {
+ url: `https://wordpress.org/wordpress-${version}.zip`,
+ version: version,
+ };
+}
diff --git a/packages/playground/cli/src/is-valid-wordpress-slug.ts b/packages/playground/cli/src/is-valid-wordpress-slug.ts
new file mode 100644
index 0000000000..4fdd957961
--- /dev/null
+++ b/packages/playground/cli/src/is-valid-wordpress-slug.ts
@@ -0,0 +1,21 @@
+/**
+ * Checks if the given version string is a valid WordPress version.
+ *
+ * The Regex is based on the releases on https://wordpress.org/download/releases/#betas
+ * The version string can be one of the following formats:
+ * - "latest"
+ * - "trunk"
+ * - "nightly"
+ * - "x.y" (x and y are integers) e.g. "6.2"
+ * - "x.y.z" (x, y and z are integers) e.g. "6.2.1"
+ * - "x.y.z-betaN" (N is an integer) e.g. "6.2.1-beta1"
+ * - "x.y.z-RCN" (N is an integer) e.g. "6.2-RC1"
+ *
+ * @param version The version string to check.
+ * @returns A boolean value indicating whether the version string is a valid WordPress version.
+ */
+export function isValidWordPressSlug(version: string): boolean {
+ const versionPattern =
+ /^latest$|^trunk$|^nightly$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
+ return versionPattern.test(version);
+}
diff --git a/packages/playground/cli/src/server.ts b/packages/playground/cli/src/server.ts
new file mode 100644
index 0000000000..55e3b99376
--- /dev/null
+++ b/packages/playground/cli/src/server.ts
@@ -0,0 +1,68 @@
+import express, { Request } from 'express';
+import { PHPRequest, PHPResponse } from '@php-wasm/universal';
+import { IncomingMessage, Server, ServerResponse } from 'http';
+import { AddressInfo } from 'net';
+
+export interface ServerOptions {
+ port: number;
+ onBind: (port: number) => Promise;
+ handleRequest: (request: PHPRequest) => Promise;
+}
+
+export async function startServer(options: ServerOptions) {
+ const app = express();
+
+ const server = await new Promise<
+ Server
+ >((resolve, reject) => {
+ const server = app.listen(options.port, () => {
+ const address = server.address();
+ if (address === null || typeof address === 'string') {
+ reject(new Error('Server address is not available'));
+ } else {
+ resolve(server);
+ }
+ });
+ });
+
+ app.use('/', async (req, res) => {
+ const phpResponse = await options.handleRequest({
+ url: req.url,
+ headers: parseHeaders(req),
+ method: req.method as any,
+ body: await bufferRequestBody(req),
+ });
+
+ res.statusCode = phpResponse.httpStatusCode;
+ for (const key in phpResponse.headers) {
+ res.setHeader(key, phpResponse.headers[key]);
+ }
+ res.end(phpResponse.bytes);
+ });
+
+ const address = server.address();
+ const port = (address! as AddressInfo).port;
+ await options.onBind(port);
+}
+
+const bufferRequestBody = async (req: Request): Promise =>
+ await new Promise((resolve) => {
+ const body: Uint8Array[] = [];
+ req.on('data', (chunk) => {
+ body.push(chunk);
+ });
+ req.on('end', () => {
+ resolve(Buffer.concat(body));
+ });
+ });
+
+const parseHeaders = (req: Request): Record => {
+ const requestHeaders: Record = {};
+ if (req.rawHeaders && req.rawHeaders.length) {
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
+ requestHeaders[req.rawHeaders[i].toLowerCase()] =
+ req.rawHeaders[i + 1];
+ }
+ }
+ return requestHeaders;
+};
diff --git a/packages/playground/cli/src/setup-php.ts b/packages/playground/cli/src/setup-php.ts
new file mode 100644
index 0000000000..4c23e34fd3
--- /dev/null
+++ b/packages/playground/cli/src/setup-php.ts
@@ -0,0 +1,140 @@
+import { NodePHP } from '@php-wasm/node';
+import {
+ BasePHP,
+ PHPRequestHandler,
+ SupportedPHPVersion,
+ __private__dont__use,
+ rotatePHPRuntime,
+} from '@php-wasm/universal';
+import { rootCertificates } from 'tls';
+import { dirname } from '@php-wasm/util';
+
+export async function createPhp(
+ requestHandler: PHPRequestHandler,
+ phpVersion: SupportedPHPVersion,
+ isPrimary: boolean
+) {
+ const createPhpRuntime = async () => await NodePHP.loadRuntime(phpVersion);
+ const php = new NodePHP();
+ php.requestHandler = requestHandler;
+ /**
+ * @TODO: Consider an API like
+ *
+ * await php.useRuntimeFactory(runtimeFactory, { rotateAfterRequests: 400 });
+ *
+ * or
+ *
+ * const php = await NodePHP.create({
+ * runtimeFactory,
+ * rotateAfterRequests: 400,
+ * })
+ */
+ php.initializeRuntime(await createPhpRuntime());
+ php.setSapiName('cli');
+ php.setPhpIniPath('/tmp/php.ini');
+ php.writeFile('/tmp/php.ini', '');
+ php.setPhpIniEntry('memory_limit', '256M');
+ php.setPhpIniEntry('allow_url_fopen', '1');
+ php.setPhpIniEntry('disable_functions', '');
+
+ // Write the ca-bundle.crt file to disk so that PHP can find it.
+ php.setPhpIniEntry('openssl.cafile', '/tmp/ca-bundle.crt');
+ php.writeFile('/tmp/ca-bundle.crt', rootCertificates.join('\n'));
+
+ if (!isPrimary) {
+ /**
+ * @TODO: Consider an API similar to
+ *
+ * php.mount('/wordpress', primaryPHP.getMountPoint('/wordpress'));
+ */
+ proxyFileSystem(
+ await requestHandler.getPrimaryPhp(),
+ php,
+ '/wordpress'
+ );
+ }
+
+ // php.setSpawnHandler(spawnHandlerFactory(processManager));
+ // Rotate the PHP runtime periodically to avoid memory leak-related crashes.
+ // @see https://github.com/WordPress/wordpress-playground/pull/990 for more context
+ rotatePHPRuntime({
+ php,
+ cwd: '/wordpress',
+ recreateRuntime: createPhpRuntime,
+ maxRequests: 400,
+ });
+ return php;
+}
+
+/**
+ * Share the parent's MEMFS instance with the child process.
+ * Only mount the document root and the /tmp directory,
+ * the rest of the filesystem (like the devices) should be
+ * private to each PHP instance.
+ *
+ * @TODO: Ship this feature in the php-wasm library. It
+ * will be commonly used in multi-instance Playground
+ * applications. The website app does something similar,
+ * and so will wp-now, VSCode, etc.
+ */
+export function proxyFileSystem(
+ sourceOfTruth: BasePHP,
+ replica: BasePHP,
+ documentRoot: string
+) {
+ for (const path of [documentRoot, '/tmp']) {
+ if (!replica.fileExists(path)) {
+ replica.mkdir(path);
+ }
+ if (!sourceOfTruth.fileExists(path)) {
+ sourceOfTruth.mkdir(path);
+ }
+ replica[__private__dont__use].FS.mount(
+ replica[__private__dont__use].PROXYFS,
+ {
+ root: path,
+ fs: sourceOfTruth[__private__dont__use].FS,
+ },
+ path
+ );
+ }
+}
+
+/**
+ * @TODO: Ship this feature in the php-wasm library.
+ *
+ * Perhaps change the implementation of the setPhpIniValue()?
+ * We could ensure there's always a valid php.ini file,
+ * even if empty. php_wasm.c wouldn't provide any defaults.
+ * BasePHP would, and it would write them to the default php.ini
+ * file. Then we'd be able to use setPhpIniValue() at any time, not
+ * just before the first run() call.
+ */
+export async function withPHPIniValues(
+ php: NodePHP,
+ phpIniValues: Record,
+ callback: () => Promise
+) {
+ const phpIniPath = (
+ await php.run({
+ code: ' `${key} = ${value}`)
+ .join('\n');
+ php.writeFile(phpIniPath, [originalPhpIni, newPhpIni].join('\n'));
+ try {
+ await callback();
+ } finally {
+ php.writeFile(phpIniPath, originalPhpIni);
+ }
+}
diff --git a/packages/playground/cli/src/setup-wp.ts b/packages/playground/cli/src/setup-wp.ts
new file mode 100644
index 0000000000..e6bfab8825
--- /dev/null
+++ b/packages/playground/cli/src/setup-wp.ts
@@ -0,0 +1,187 @@
+import fs from 'fs';
+
+import { NodePHP } from '@php-wasm/node';
+import { EmscriptenDownloadMonitor } from '@php-wasm/progress';
+import {
+ defineSiteUrl,
+ runWpInstallationWizard,
+ unzip,
+ zipWpContent,
+} from '@wp-playground/blueprints';
+import path from 'path';
+import {
+ resolveWPRelease,
+ cachedDownload,
+ CACHE_FOLDER,
+ readAsFile,
+} from './download';
+import { withPHPIniValues } from './setup-php';
+
+/**
+ * Ensures a functional WordPress installation in php document root.
+ *
+ * This is a TypeScript function for now, just to get something off the
+ * ground, but it will be superseded by the PHP Blueprints library developed
+ * at https://github.com/WordPress/blueprints-library/
+ *
+ * That PHP library will come with a set of functions and a CLI tool to
+ * turn a Blueprint into a WordPress directory structure or a zip Snapshot.
+ * Let's **not** invest in the TypeScript implementation of this function,
+ * accept the limitation, and switch to the PHP implementation as soon
+ * as that's viable.
+ */
+export async function setupWordPress(
+ php: NodePHP,
+ wpVersion = 'latest',
+ monitor: EmscriptenDownloadMonitor
+) {
+ /**
+ * @TODO: This looks similar to what the website does to setup WordPress.
+ * Perhaps there's a common function that could be shared?
+ */
+ const wpDetails = await resolveWPRelease(wpVersion);
+ const [wpZip, sqliteZip] = await Promise.all([
+ cachedDownload(wpDetails.url, `${wpDetails.version}.zip`, monitor),
+ cachedDownload(
+ 'https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip',
+ 'sqlite.zip',
+ monitor
+ ),
+ ]);
+ await prepareWordPress(php, wpZip, sqliteZip);
+
+ const preinstalledWpContentPath = path.join(
+ CACHE_FOLDER,
+ `prebuilt-wp-content-for-wp-${wpDetails.version}.zip`
+ );
+ if (fs.existsSync(preinstalledWpContentPath)) {
+ /**
+ * @TODO: This caching mechanism will be made generic and provided as a
+ * handler for the PHP Blueprints library.
+ */
+ await unzip(php, {
+ zipFile: readAsFile(preinstalledWpContentPath),
+ extractToPath: '/wordpress',
+ });
+ } else {
+ // Define a fake URL for the installation wizard.
+ await defineSiteUrl(php, {
+ siteUrl: 'http://playground.internal',
+ });
+
+ // Disable networking for the installation wizard
+ // to avoid loopback requests and also speed it up.
+ // @TODO: Expose withPHPIniValues as a function from the
+ // php-wasm library.
+ await withPHPIniValues(
+ php,
+ {
+ disable_functions: 'fsockopen',
+ allow_url_fopen: '0',
+ },
+ async () =>
+ await runWpInstallationWizard(php, {
+ options: {},
+ })
+ );
+
+ const wpContent = await zipWpContent(php);
+ fs.writeFileSync(preinstalledWpContentPath, wpContent);
+ }
+}
+
+/**
+ * Prepare the WordPress document root given a WordPress zip file and
+ * the sqlite-database-integration zip file.
+ *
+ * This is a TypeScript function for now, just to get something off the
+ * ground, but it will be superseded by the PHP Blueprints library developed
+ * at https://github.com/WordPress/blueprints-library/
+ *
+ * That PHP library will come with a set of functions and a CLI tool to
+ * turn a Blueprint into a WordPress directory structure or a zip Snapshot.
+ * Let's **not** invest in the TypeScript implementation of this function,
+ * accept the limitation, and switch to the PHP implementation as soon
+ * as that's viable.
+ */
+async function prepareWordPress(php: NodePHP, wpZip: File, sqliteZip: File) {
+ php.mkdir('/tmp/unzipped-wordpress');
+ await unzip(php, {
+ zipFile: wpZip,
+ extractToPath: '/tmp/unzipped-wordpress',
+ });
+ // The zip file may contain a subdirectory, or not.
+ const wpPath = php.fileExists('/tmp/unzipped-wordpress/wordpress')
+ ? '/tmp/unzipped-wordpress/wordpress'
+ : '/tmp/unzipped-wordpress';
+ php.mv(wpPath, '/wordpress');
+
+ php.mkdir('/tmp/sqlite-database-integration');
+ await unzip(php, {
+ zipFile: sqliteZip,
+ extractToPath: '/tmp/sqlite-database-integration',
+ });
+
+ php.mv(
+ '/tmp/sqlite-database-integration/sqlite-database-integration-main',
+ '/wordpress/sqlite-database-integration'
+ );
+
+ const db = php.readFileAsText(
+ '/wordpress/sqlite-database-integration/db.copy'
+ );
+ const updatedDb = db
+ .replace(
+ "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'",
+ "__DIR__.'/../sqlite-database-integration/'"
+ )
+ .replace(
+ "'{SQLITE_PLUGIN}'",
+ "__DIR__.'/../sqlite-database-integration/load.php'"
+ );
+ php.writeFile('/wordpress/wp-content/db.php', updatedDb);
+
+ /**
+ * This should be a mu-plugin, but since the user may have
+ * provided custom mounts, we avoid writing to the mu-plugins
+ * directory
+ *
+ * @TODO: Either document this hack, or find a better way to
+ * handle this.
+ * @TODO: The web version also uses an mu-plugin and it has the
+ * same filters as this one. Also wp-now and VS Code.
+ * There seems to be an opportunity to share the code between
+ * the two.
+ */
+
+ php.writeFile(
+ '/wordpress/wp-includes/default-filters.php',
+ php.readFileAsText('/wordpress/wp-includes/default-filters.php') +
+ `
+ // Redirect /wp-admin to /wp-admin/
+ add_filter( 'redirect_canonical', function( $redirect_url ) {
+ if ( '/wp-admin' === $redirect_url ) {
+ return $redirect_url . '/';
+ }
+ return $redirect_url;
+ } );
+
+ // Needed because gethostbyname( 'wordpress.org' ) returns
+ // a private network IP address for some reason.
+ add_filter( 'allowed_redirect_hosts', function( $deprecated = '' ) {
+ return array(
+ 'wordpress.org',
+ 'api.wordpress.org',
+ 'downloads.wordpress.org',
+ );
+ } );
+
+ // Support permalinks without "index.php"
+ add_filter( 'got_url_rewrite', '__return_true' );
+ `
+ );
+ php.writeFile(
+ '/wordpress/wp-config.php',
+ php.readFileAsText('/wordpress/wp-config-sample.php')
+ );
+}
diff --git a/packages/playground/cli/tsconfig.json b/packages/playground/cli/tsconfig.json
new file mode 100644
index 0000000000..e4e0c46e69
--- /dev/null
+++ b/packages/playground/cli/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "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/playground/cli/tsconfig.lib.json b/packages/playground/cli/tsconfig.lib.json
new file mode 100644
index 0000000000..829b0bc14c
--- /dev/null
+++ b/packages/playground/cli/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/packages/playground/cli/tsconfig.spec.json b/packages/playground/cli/tsconfig.spec.json
new file mode 100644
index 0000000000..231650b3da
--- /dev/null
+++ b/packages/playground/cli/tsconfig.spec.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/packages/playground/cli/vite.config.ts b/packages/playground/cli/vite.config.ts
new file mode 100644
index 0000000000..ca7e3aa652
--- /dev/null
+++ b/packages/playground/cli/vite.config.ts
@@ -0,0 +1,56 @@
+///
+import { defineConfig } from 'vite';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+
+export default defineConfig({
+ assetsInclude: ['**/*.ini'],
+ cacheDir: '../../../node_modules/.vite/php-cli',
+
+ plugins: [nxViteTsPaths()],
+
+ // Configuration for building your library.
+ // See: https://vitejs.dev/guide/build.html#library-mode
+ build: {
+ assetsInlineLimit: 0,
+ target: 'es2020',
+ rollupOptions: {
+ external: [
+ '@php-wasm/node',
+ '@php-wasm/universal',
+ '@php-wasm/logger',
+ '@php-wasm/progress',
+ '@php-wasm/util',
+ '@wp-playground/wordpress',
+ '@wp-playground/blueprints',
+ 'yargs',
+ 'express',
+ 'os',
+ 'net',
+ 'fs',
+ 'fs-extra',
+ 'path',
+ 'child_process',
+ 'http',
+ 'path',
+ 'tls',
+ 'util',
+ 'dns',
+ 'ws',
+ ],
+ input: 'packages/playground/cli/src/cli.ts',
+ output: {
+ format: 'esm',
+ entryFileNames: '[name].js',
+ },
+ },
+ },
+
+ test: {
+ globals: true,
+ cache: {
+ dir: '../../../node_modules/.vitest',
+ },
+ environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ },
+});
diff --git a/packages/playground/client/package.json b/packages/playground/client/package.json
index a906d9ddba..7c03025ddd 100644
--- a/packages/playground/client/package.json
+++ b/packages/playground/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@wp-playground/client",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "WordPress Playground client",
"repository": {
"type": "git",
diff --git a/packages/playground/sync/vite.config.ts b/packages/playground/sync/vite.config.ts
index cc65b73ecf..c5ab5cae07 100644
--- a/packages/playground/sync/vite.config.ts
+++ b/packages/playground/sync/vite.config.ts
@@ -1,8 +1,9 @@
///
// eslint-disable-next-line @nx/enforce-module-boundaries
import { viteTsConfigPaths } from '../../vite-ts-config-paths';
+
// eslint-disable-next-line @nx/enforce-module-boundaries
-import ignoreWasmImports from '../ignore-wasm-imports';
+import { viteWasmLoader } from '../../vite-wasm-loader';
export default {
base: '/',
@@ -19,7 +20,7 @@ export default {
viteTsConfigPaths({
root: '../../../',
}),
- ignoreWasmImports(),
+ viteWasmLoader,
],
// Configuration for building your library.
diff --git a/packages/playground/wordpress/package.json b/packages/playground/wordpress/package.json
index a26098e0ac..b899762efc 100644
--- a/packages/playground/wordpress/package.json
+++ b/packages/playground/wordpress/package.json
@@ -16,7 +16,6 @@
}
],
"main": "./index.js",
- "module": "./index.mjs",
"typings": "./index.d.ts",
"license": "GPL-2.0-or-later",
"type": "module",
diff --git a/packages/vite-wasm-loader.ts b/packages/vite-wasm-loader.ts
new file mode 100644
index 0000000000..28a2975ba4
--- /dev/null
+++ b/packages/vite-wasm-loader.ts
@@ -0,0 +1,7 @@
+export const viteWasmLoader = {
+ load(id: string): any {
+ if (id.endsWith('.wasm')) {
+ return `export default ${JSON.stringify(id)}`;
+ }
+ },
+} as any;
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 2ba54cf630..a3bfe1f160 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -42,6 +42,7 @@
"@wp-playground/blueprints": [
"packages/playground/blueprints/src/index.ts"
],
+ "@wp-playground/cli": ["packages/playground/cli/src/index.ts"],
"@wp-playground/client": [
"packages/playground/client/src/index.ts"
],