Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playground Blueprints #211

Merged
merged 5 commits into from Apr 21, 2023
Merged

Playground Blueprints #211

merged 5 commits into from Apr 21, 2023

Conversation

adamziel
Copy link
Collaborator

@adamziel adamziel commented Apr 18, 2023

Description

Adds Blueprints – a declarative data format for setting up new Playground instances. The big idea is:

  • @wp-playground/client has python-like batteries, e.g. installPlugin, login, importFile etc
  • The Blueprint is a JSON file that says which of these batteries to call and in which order
  • In addition, Blueprint can invoke specific lower-level functions like php.writeFile
  • External resources are declared upfront and fetch()-ed in parallel

Because Blueprints merely invoke the existing Playground client functions, an advanced user may choose to not use them at all and setup their Playground using a bootstrap script that would invoke the same publicly exported functions.

Blueprints have three advantages over using a custom JavaScript file like setup.js:

  • You don’t need to run untrusted JS code – handy for wp.org or any case of sharing blueprints
  • It automatically fetches files and updates progress bars
  • In the future, there will be a mergeBlueprints() function to turn a list of Blueprints into a single Blueprint. Technically, it will concatenate the steps, parallelize resource fetching, and resolve any PHP and WP version constraints from all the merged Blueprints.

Example

The new loadPlayground helper makes setting up a Playground instance as easy as:

const playground = await loadPlayground({
	iframe,
	remoteUrl: `https://playground.wordpress.net/remote.html`,
	blueprint: {
		"landingPage": "/wp-admin/",
		"preferredVersions": {
			"php": "8.0",
			"wp": "latest"
		},
		"steps": [
			{ "step": "login", "username": "admin", "password": "password" },
			{
				"step": "installPlugin",
				"pluginZipFile": { "resource": "wordpress.org/plugins", "slug": "friends" }
			}
		]
	}
})

Here's a longer example. The following blueprint logs you in as admin, installs the twentytwentytwo theme, installs the gutenberg plugin, and redirects the user to /wp-admin.

{
	"landingPage": "/wp-admin/?welcome=0",
	"preferredVersions": {
		"php": "8.0",
		"wp": "latest"
	},
	"steps": [
		{
			"step": "login",
			"username": "admin",
			"password": "password"
		},
		{
			"step": "mkdir",
			"path": "/wordpress/wp-content/languages/plugins"
		},
		{
			"step": "writeFile",
			"path": "/wordpress/wp-content/languages/plugin-name/en.json",
			"data": {
				"resource": "url",
				"caption": "Downloading plugin-name language file",
				"url": "https://example.com/plugin-name/en.json"
			}
		},
		{
			"step": "setSiteOptions",
			"options": {
				"WPLANG": "en",
				"permalink_structure": "/%year%/%monthnum%/%day%/%postname%/",
				"gp_enable_local_translation": 1,
				"gp_enable_inline_translation": 1
			}
		},
		{
			"step": "updateUserMeta",
			"meta": {
				"show_welcome_panel": "0"
			},
			"userId": 1
		},
		{
			"step": "writeFile",
			"path": "/wordpress/wp-content/mu-plugins/gp-sqlite.php",
			"data": "<?php\n\tadd_filter('query', function( $query ) {\n\t\treturn str_replace( ' BINARY ', ' ', $query);\n\t} );\n"
		},
		{
			"step": "installPlugin",
			"pluginZipFile": {
				"resource": "wordpress.org/plugins",
				"slug": "glotpress-local"
			}
		}
	]
}

Steps

For the first version, Blueprints support the following steps:

export type Step<ResourceType> = (
	| {
			step: 'installPlugin';
			pluginZipFile: ResourceType;
			options?: InstallPluginOptions;
	  }
	| {
			step: 'installTheme';
			themeZipFile: ResourceType;
			options?: InstallThemeOptions;
	  }
	| {
			step: 'login';
			username?: string;
			password?: string;
	  }
	| {
			step: 'importFile';
			file: ResourceType;
	  }
	| {
			step: 'activatePlugin';
			plugin: string;
	  }
	| {
			step: 'replaceSite';
			fullSiteZip: ResourceType;
	  }
	| {
			step: 'unzip';
			zipPath: string;
			extractToPath: string;
	  }
	| {
			step: 'setSiteOptions';
			options: SiteOptions;
	  }
	| {
			step: 'updateUserMeta';
			meta: UserMeta;
			userId: number;
	  }
	| {
			step: 'runPHP';
			code: string;
	  }
	| {
			step: 'runPHPWithOptions';
			options?: PHPRunOptions;
	  }
	| {
			step: 'setPhpIniEntry';
			key: string;
			value: string;
	  }
	| {
			step: 'request';
			request: PHPRequest;
			maxRedirects?: number;
	  }
	| {
			step: 'cp';
			fromPath: string;
			toPath: string;
	  }
	| {
			step: 'mv';
			fromPath: string;
			toPath: string;
	  }
	| {
			step: 'mkdir';
			path: string;
	  }
	| {
			step: 'rm';
			path: string;
	  }
	| {
			step: 'rmdir';
			path: string;
	  }
	| {
			step: 'fetch';
			url: string;
			path: string;
	  }
	| {
			step: 'writeFile';
			path: string;
			data: ResourceType | string | Uint8Array;
	  }

Other notes

This PR also moves the progress bar UI component from Playground Website to remote.html to make reusable in other apps. Virtually every Playground consumer will want to display a progress bar, there's no need to ask everyone to reimplement it when Playground can provide a convenient API.

Solves #201
Related to #115

cc @bengreeley @akirk @dmsnell @StevenDufresne

@adamziel adamziel added [Type] Enhancement New feature or request Importing labels Apr 18, 2023
@adamziel adamziel self-assigned this Apr 18, 2023
@adamziel
Copy link
Collaborator Author

I simplified the JSON structure to remove "resources" and started on automatic progress management, applying a blueprint will now provide progress % and a human-readable caption. Some more changes incoming.

@adamziel
Copy link
Collaborator Author

I'm noodling on setting up GlotPress playground using blueprints, here's what I have so far:
https://gist.github.com/adamziel/d3270b8ce1f18b465bdcbc2dcd103f4d

@adamziel
Copy link
Collaborator Author

adamziel commented Apr 21, 2023

OK I think this is ready! See the updated example in the PR description. I'm planning to split this into smaller changesets and start merging them. If you were holding back with any feedback, now is a good time to share it.

@adamziel adamziel force-pushed the explore-blueprints branch 4 times, most recently from f897c6e to ecf1180 Compare April 21, 2023 18:26
@adamziel adamziel marked this pull request as ready for review April 21, 2023 18:30
@adamziel
Copy link
Collaborator Author

adamziel commented Apr 21, 2023

We're down to 27 updated files from 85 in the beginning. I'd say that's good enough. Let's merge!

@adamziel adamziel merged commit b031445 into trunk Apr 21, 2023
1 check passed
@adamziel adamziel deleted the explore-blueprints branch April 21, 2023 18:42
adamziel added a commit that referenced this pull request Apr 24, 2023
## Description

Generalizes [Playground Blueprints](#211) from working with just the in-browser Playground API client to working:

* On the web and in Node.js
* With a local Playground object
* With a remote Playground client

With this PR applied, all of the following `login()` calls are valid:

```ts
// In the browser
const phpInSameThread = await WebPHP.load( '7.4', { dataModules: [ 'wp.data' ] } );
await login( phpInSameThread );

const phpInWorker = await consumeAPI( playgroundIframe );
await login( phpInWorker );
```

```ts
// In node.js
const phpInSameThread = await NodePHP.load( '7.4' );
phpInSameThread.mount( '/wordpress', '/wordpress' );
await login( phpInSameThread );
// ^ @todo: Still fails unless you provide a DOMParser polyfill
```

This opens the door to using Blueprints in the VS Code extension, wp-now, and other tools.

## Implementation

Blueprint were initially implemented as a part of the browser API client in `@wp-playground/client`. This PR decouples them into an isomorphic `@wp-playground/blueprints` package that depends on `@php-wasm/universal` which is also isomorphic.

In other words, step handlers such as `login(playground)` used to require a `PlaygroundClient` instance, but now they can work with a `UniversalPHP` instance defined as follows:

```ts
type IsomorphicLocalPHP = { /* ... PHP methods ... */ }
// Remote<T> means every method of T now returns a promise
type IsomorphicRemotePHP = Remote<IsomorphicLocalPHP>;
type UniversalPHP = IsomorphicLocalPHP | IsomorphicRemotePHP;
```

`UniversalPHP` is a type, not a class. It's a common core of all PHP implementations in other packages and provides methods like `run()`, `request()`, and `writeFile()`. `@php-wasm/universal` also provides a reference implementation of `UniversalPHP` called `BasePHP`.

`BasePHP` cannot be used directly. Instead, platform-specific packages `@php-wasm/web` and `@php-wasm/node` provide platform-specific implementations. The former exports `WebPHP`, which loads files using `fetch()`, and the latter exports `NodePHP`, which reads data directly from the host filesystem. Both implement the `UniversalPHP` interface and can be used with any Blueprint step.

## Other notes

* `@php-wasm/universal`, `@wp-playground/client`, and `@wp-playground/blueprints` are published as isomorphic ESM/CJS packages. `@php-wasm/node` is published as CJS only for now.

## Follow-up work

* `@wp-playground/blueprints` will need to be smart about providing a Node.js polyfill for `new DOMParser()` and `fetch()`.
@adamziel adamziel mentioned this pull request Apr 27, 2023
56 tasks
Pookie717 added a commit to Pookie717/wordpress-playground that referenced this pull request Oct 1, 2023
## Description

Generalizes [Playground Blueprints](WordPress/wordpress-playground#211) from working with just the in-browser Playground API client to working:

* On the web and in Node.js
* With a local Playground object
* With a remote Playground client

With this PR applied, all of the following `login()` calls are valid:

```ts
// In the browser
const phpInSameThread = await WebPHP.load( '7.4', { dataModules: [ 'wp.data' ] } );
await login( phpInSameThread );

const phpInWorker = await consumeAPI( playgroundIframe );
await login( phpInWorker );
```

```ts
// In node.js
const phpInSameThread = await NodePHP.load( '7.4' );
phpInSameThread.mount( '/wordpress', '/wordpress' );
await login( phpInSameThread );
// ^ @todo: Still fails unless you provide a DOMParser polyfill
```

This opens the door to using Blueprints in the VS Code extension, wp-now, and other tools.

## Implementation

Blueprint were initially implemented as a part of the browser API client in `@wp-playground/client`. This PR decouples them into an isomorphic `@wp-playground/blueprints` package that depends on `@php-wasm/universal` which is also isomorphic.

In other words, step handlers such as `login(playground)` used to require a `PlaygroundClient` instance, but now they can work with a `UniversalPHP` instance defined as follows:

```ts
type IsomorphicLocalPHP = { /* ... PHP methods ... */ }
// Remote<T> means every method of T now returns a promise
type IsomorphicRemotePHP = Remote<IsomorphicLocalPHP>;
type UniversalPHP = IsomorphicLocalPHP | IsomorphicRemotePHP;
```

`UniversalPHP` is a type, not a class. It's a common core of all PHP implementations in other packages and provides methods like `run()`, `request()`, and `writeFile()`. `@php-wasm/universal` also provides a reference implementation of `UniversalPHP` called `BasePHP`.

`BasePHP` cannot be used directly. Instead, platform-specific packages `@php-wasm/web` and `@php-wasm/node` provide platform-specific implementations. The former exports `WebPHP`, which loads files using `fetch()`, and the latter exports `NodePHP`, which reads data directly from the host filesystem. Both implement the `UniversalPHP` interface and can be used with any Blueprint step.

## Other notes

* `@php-wasm/universal`, `@wp-playground/client`, and `@wp-playground/blueprints` are published as isomorphic ESM/CJS packages. `@php-wasm/node` is published as CJS only for now.

## Follow-up work

* `@wp-playground/blueprints` will need to be smart about providing a Node.js polyfill for `new DOMParser()` and `fetch()`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants