#CFHack2023: WordPress in VS Code!!!

At the CloudFest Hackathon over the weekend, we embedded WordPress directly into VS Code.

Forget spending hours setting up a local development environment at your next Contributor Day. Simply install the WordPress Playground VS Code extension, run “Launch WordPress Playground” from the command launcher, and you’ll have a fully mostly functional WordPress installation right inside your editor.

Disclaimer: This extension was built during a hack weekend and hasn’t been thoroughly tested. It exists for demonstration purposes. Check out our project repository for a deeper dive into the codebase, and to report any bugs you encounter.

If it seems like magic… that’s because it is. The WordPress Playground extension builds on top of WordPress Playground, which uses WebAssembly to run WordPress entirely within the browser.

“Even PHP code?” you might think, and the answer is “yes”. WordPress Playground compiles PHP to WebAssembly using Emscripten. This compiled WebAssembly file can then execute any PHP code in your project.

Let’s take a journey through how the VS Code extension works!

First, it imports a few magical utilities from WordPress Playground. (Normally, you’d pull these from the published ‘@php-wasm/node’ library, but we needed to compile our own versions with the networking proxy disabled and using the virtualized filesystem.) These utilities bring in the underlying WebAssembly magic.

import { PHP, PHPServer, loadPHPRuntime, getPHPLoaderModule, PHPBrowser } from './built-php-wasm-node';

With the utilities, our extension instantiates a PHPBrowser object by:

  • Initializing PHP WebAssembly.
  • Loading the bundled WordPress files into PHP WebAssembly. The bundle includes a SQLite database with WordPress already installed, as well as a few other necessary hacks.
  • Hacking WordPress even further to get it to work (network requests don’t work all that well right now).
  • Initializing a PHPBrowser object and logging in as our default user.
async function loadPhpBrowser( context: vscode.ExtensionContext, openPort: number, pluginPath: string, phpVersion: string='8.0') {
	const phpLoaderModule = await getPHPLoaderModule(phpVersion);
	const loaderId = await loadPHPRuntime(phpLoaderModule);
	const php = new PHP(loaderId);
	const wordpressZip = fs.readFileSync( context.extensionPath + '/dist/wordpress.zip' );
	php.writeFile( '/wordpress.zip', wordpressZip );
	const databaseFromZipFileReadRequest = php.run({
		code: importPhp 
		+ ` importZipFile( '/wordpress.zip' );`,
	});
	if ( pluginPath ) {
		php.mkdirTree( `/wordpress/wp-content/plugins/${path.basename( pluginPath )}` );
		php.mount({root: pluginPath} as any, `/wordpress/wp-content/plugins/${path.basename( pluginPath )}` );
	}
	patchWordPress(php);
	const phpServer = new PHPServer(php, {
		documentRoot: '/wordpress',
		absoluteUrl: `http://localhost:${openPort}/`,
		isStaticFilePath: (path: string) => {
			const fullPath = '/wordpress' + path;
			return php.fileExists(fullPath)
				&& ! php.isDir(fullPath)
				&& ! seemsLikeAPHPFile(fullPath);
		}
	});
	const browser = new PHPBrowser( phpServer );
	await login( browser );
	return browser;
}

Once PHPBrowser is ready, our extension launches a Node web server and uses it to resolve localhost requests. We chose this approach, instead of running WordPress Playground entirely in the browser, because Node has access to your project’s files. This makes it easy to load your plugin into WordPress Playground.

const server = http.createServer( async (req : any, res : any) => {
    let requestHeaders: { [ key: string ]: string } = {};
    if ( req.rawHeaders && req.rawHeaders.length ) {
   	 for ( let i = 0; i < req.rawHeaders.length; i += 2 ) {
   		 requestHeaders[ req.rawHeaders[ i ] ] = req.rawHeaders[ i + 1 ];
   	 }
    }
    phpBrowser = await loadPhpBrowser( context, openPort, pluginPath, '8.0');
    const reqBody = await new Promise( (resolve, reject) => {
   	 let body = '';
   	 req.on('data', chunk => {
   		 body += chunk.toString();
   	 });
   	 req.on('end', () => {
   		 resolve(body);
   	 });
    });

    const resp = await phpBrowser.request( {
   	 relativeUrl: req.url,
   	 headers: requestHeaders,
   	 method: req.method,
   	 body: reqBody,
    } );

    res.statusCode = resp.httpStatusCode;
    Object.keys(resp.headers).forEach((key) => {
   	 res.setHeader(key, resp.headers[key]);
    });
    res.end(resp.body);
});

Last, we have a little React app that loads the localhost URL inside of an iframe and adds some nice chrome around it.

const panel = vscode.window.createWebviewPanel(
    'playgroundviewer',
    'WordPress Playground',
    vscode.ViewColumn.One,
    {
   	 enableScripts: true,
    }
    );
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'dist', 'playground-website.js');
const websiteJsSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = `
    <!DOCTYPE html>
    <html>
    <head>
   	 <meta charset="UTF-8">
   	 <meta name="viewport" content="width=device-width, initial-scale=1.0">
   	 <title>WordPress Playground</title>
    </head>
    <body class="with-background">
   	 <div id="root" data-iframe-src="https://localhost:${openPort}/"></div>
   	 <script src="${websiteJsSrc}"></script>
    </body>
    </html>
`;

Et voilà! WordPress in VS Code.

Many thanks to Adam Zieliński, Daniel Bachhuber, Adrian Stobbe, Eric Binnion, Florian Blaser, Greg Ziółkowski, Héctor Prieto, Mohannad Rahmani, and Pascal Birchler for their tireless work over the weekend.