Skip to content

Commit

Permalink
Isomorphic PHP, Isomorphic Blueprints (#214)
Browse files Browse the repository at this point in the history
## 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()`.
  • Loading branch information
adamziel committed Apr 24, 2023
1 parent b031445 commit 34695fb
Show file tree
Hide file tree
Showing 115 changed files with 2,873 additions and 2,008 deletions.
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": []
"depConstraints": [
{
"sourceTag": "*",
"notDependOnLibsWithTags": ["scope:web-client"]
}
]
}
],
"no-inner-declarations": 0,
Expand Down
5 changes: 5 additions & 0 deletions packages/nx-extensions/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"implementation": "./src/executors/package-json/executor",
"schema": "./src/executors/package-json/schema.json",
"description": "package-json executor"
},
"assert-built-esm-and-cjs": {
"implementation": "./src/executors/assert-built-esm-and-cjs/executor",
"schema": "./src/executors/assert-built-esm-and-cjs/schema.json",
"description": "assert-built-esm-and-cjs executor"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ExecutorContext } from '@nrwl/devkit';
import { spawn } from 'child_process';
import { mkdirSync, writeFileSync } from 'fs';
import * as path from 'path';
import { AssertBuiltEsmAndCjsExecutorSchema } from './schema';

/**
* Test whether a module can be imported as both ESM and CJS.
*
* @param options
* @param context
* @returns
*/
export default async function runExecutor(
options: AssertBuiltEsmAndCjsExecutorSchema,
context: ExecutorContext
) {
const buildDir = options.outputPath.split('/')[0];
const testsPath = path.join(context.root, buildDir, 'test-esm-cjs');
mkdirSync(testsPath, { recursive: true });

writeFileSync(
path.join(testsPath, 'test-esm.mjs'),
`import * as result from '../../${options.outputPath}/index.js';`
);
writeFileSync(
path.join(testsPath, 'test-cjs.cjs'),
`require('../../${options.outputPath}');`
);
const checkForSuccess = (scriptName) =>
new Promise((resolve, reject) => {
const test = spawn('node', [scriptName], {
cwd: testsPath,
stdio: 'pipe',
});

let stdout = '';
test.stdout!.on('data', (chunk) => (stdout += chunk));

let stderr = '';
test.stderr!.on('data', (chunk) => (stderr += chunk));

test.on('close', (statusCode) => {
return statusCode === 0
? resolve(stdout)
: reject(
`${context.targetName} could not be imported as both ESM and CJS: ${stdout} ${stderr}`
);
});
});

await checkForSuccess('test-esm.mjs');
await checkForSuccess('test-cjs.cjs');
return { success: true };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface AssertBuiltEsmAndCjsExecutorSchema {
outputPath: string;
} // eslint-disable-line
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"cli": "nx",
"title": "AssertBuiltEsmAndCjs executor",
"description": "",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "The path to the built module"
}
},
"required": ["outputPath"]
}
12 changes: 0 additions & 12 deletions packages/php-wasm/abstract/README.md

This file was deleted.

39 changes: 0 additions & 39 deletions packages/php-wasm/abstract/project.json

This file was deleted.

0 comments on commit 34695fb

Please sign in to comment.