Skip to content

Commit

Permalink
Merge branch 'trunk' into fix/1580-fix-npm-vulnerabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
bgrgicak authored Jul 31, 2024
2 parents 44d2cff + 0a9f3e4 commit 95552a0
Show file tree
Hide file tree
Showing 65 changed files with 54,812 additions and 1,915 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/build-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/prepare-playground
- run: npm run build
- run: tar -czf wasm-wordpress-net.tar.gz dist/packages/playground/wasm-wordpress-net
# Store dist/packages/artifacts/wasm-wordpress-net as a build artifact
- uses: actions/upload-artifact@v2
with:
name: playground-website
path: wasm-wordpress-net.tar.gz
# The artifact only becomes available after this workflow wraps up, so let's wrap.
# This artifact enables the download of a pre-built package and hosting of one's own playground instance.

- name: Deploy to playground.wordpress.net
shell: bash
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,35 @@ PHP=7.4 npx @php-wasm/cli -v
npx @php-wasm/cli phpcbf
```

### Test offline support

To test the offline support you need to build the website and run a local server:

```bash
PLAYGROUND_URL=http://localhost:9999 npx nx run playground-website:build:wasm-wordpress-net
```

Then you can run a local server:

```bash
php -S localhost:9999 -t dist/packages/playground/wasm-wordpress-net
```

or using Docker:

```bash
docker run --rm -p 9999:80 -v $(pwd)/dist/packages/playground/wasm-wordpress-net:/usr/share/nginx/html:ro nginx:alpine
```

#### Using the browser to test the offline support

1. Open the browser and go to `http://localhost:9999`.
2. Open the browser's developer tools.
3. Go to the "Network" tab.
4. Select "Offline" in the throttling dropdown menu.
5. Refresh the page.


## How can I contribute?

WordPress Playground is an open-source project and welcomes all contributors from code to design, and from documentation to triage. If the feature you need is missing, you are more than welcome to start a discussion, open an issue, and even propose a Pull Request to implement it.
Expand Down
6 changes: 6 additions & 0 deletions packages/php-wasm/node/src/test/php.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,12 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => {
});
});

describe('Interface', () => {
it('run() should throw an error when neither `code` nor `scriptFile` is provided', async () => {
expect(() => php.run({})).rejects.toThrowError(TypeError);
});
});

describe('Startup sequence – basics', () => {
/**
* This test ensures that the PHP runtime can be loaded twice.
Expand Down
68 changes: 67 additions & 1 deletion packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('rotatePHPRuntime()', () => {
expect(recreateRuntimeSpy).toHaveBeenCalledTimes(2);
}, 30_000);

it('Should stop rotating after the cleanup handler is called', async () => {
it('Should not rotate after the cleanup handler is called, even if max requests is reached', async () => {
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
const cleanup = rotatePHPRuntime({
Expand All @@ -88,6 +88,72 @@ describe('rotatePHPRuntime()', () => {
expect(recreateRuntimeSpy).toHaveBeenCalledTimes(2);
}, 30_000);

it('Should recreate the PHP runtime after a PHP runtime crash', async () => {
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
rotatePHPRuntime({
php,
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1234,
});
// Cause a PHP runtime rotation due to error
await php.dispatchEvent({
type: 'request.error',
error: new Error('mock error'),
source: 'php-wasm',
});
expect(recreateRuntimeSpy).toHaveBeenCalledTimes(2);
}, 30_000);

it('Should not recreate the PHP runtime after a PHP fatal', async () => {
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
rotatePHPRuntime({
php,
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1234,
});
// Trigger error with no `source`
await php.dispatchEvent({
type: 'request.error',
error: new Error('mock error'),
});
// Trigger error with request `source`
await php.dispatchEvent({
type: 'request.error',
error: new Error('mock error'),
source: 'request',
});
expect(recreateRuntimeSpy).toHaveBeenCalledTimes(1);
}, 30_000);

it('Should not rotate after the cleanup handler is called, even if there is a PHP runtime error', async () => {
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
const cleanup = rotatePHPRuntime({
php,
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1,
});
// Rotate the PHP runtime
await php.run({ code: `` });
expect(recreateRuntimeSpy).toHaveBeenCalledTimes(2);

cleanup();

// No further rotation should happen
php.dispatchEvent({
type: 'request.error',
error: new Error('mock error'),
source: 'php-wasm',
});

expect(recreateRuntimeSpy).toHaveBeenCalledTimes(2);
}, 30_000);

it('Should hotswap the PHP runtime from 8.2 to 8.3', async () => {
let nbCalls = 0;
const recreateRuntimeSpy = vitest.fn(() => {
Expand Down
7 changes: 6 additions & 1 deletion packages/php-wasm/universal/src/lib/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,13 @@ export class PHP implements Disposable {
if (typeof request.code === 'string') {
this.writeFile('/internal/eval.php', request.code);
this.#setScriptPath('/internal/eval.php');
} else {
} else if (typeof request.scriptPath === 'string') {
this.#setScriptPath(request.scriptPath || '');
} else {
throw new TypeError(
'The request object must have either a `code` or a ' +
'`scriptPath` property.'
);
}

const $_SERVER = this.#prepareServerEntries(
Expand Down
32 changes: 24 additions & 8 deletions packages/php-wasm/universal/src/lib/rotate-php-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PHP } from './php';
import { PHPEvent } from './universal-php';

export interface RotateOptions {
php: PHP;
Expand Down Expand Up @@ -37,22 +38,37 @@ export function rotatePHPRuntime({
*/
maxRequests = 400,
}: RotateOptions) {
let handledCalls = 0;
let runtimeRequestCount = 0;
async function rotateRuntime() {
if (++handledCalls < maxRequests) {
return;
}
handledCalls = 0;

const release = await php.semaphore.acquire();
try {
php.hotSwapPHPRuntime(await recreateRuntime(), cwd);

// A new runtime has handled zero requests.
runtimeRequestCount = 0;
} finally {
release();
}
}
php.addEventListener('request.end', rotateRuntime);

async function rotateRuntimeAfterMaxRequests() {
if (++runtimeRequestCount < maxRequests) {
return;
}
await rotateRuntime();
}

async function rotateRuntimeForPhpWasmError(event: PHPEvent) {
if (event.type === 'request.error' && event.source === 'php-wasm') {
await rotateRuntime();
}
}

php.addEventListener('request.error', rotateRuntimeForPhpWasmError);
php.addEventListener('request.end', rotateRuntimeAfterMaxRequests);

return function () {
php.removeEventListener('request.end', rotateRuntime);
php.removeEventListener('request.error', rotateRuntimeForPhpWasmError);
php.removeEventListener('request.end', rotateRuntimeAfterMaxRequests);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ export async function broadcastMessageExpectReply(message: any, scope: string) {
}

interface ServiceWorkerConfiguration {
cacheVersion: string;
handleRequest?: (event: FetchEvent) => Promise<Response> | undefined;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/php-wasm/web-service-worker/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"include": [
"src/**/*.ts",
],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
11 changes: 10 additions & 1 deletion packages/php-wasm/web/src/lib/register-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PHPWorker } from '@php-wasm/universal';
import { PhpWasmError } from '@php-wasm/util';
import { responseTo } from '@php-wasm/web-service-worker';
import { Remote } from 'comlink';
import { logger } from '@php-wasm/logger';

export interface Client extends Remote<PHPWorker> {}

Expand Down Expand Up @@ -65,7 +66,15 @@ export async function registerServiceWorker(scope: string, scriptUrl: string) {

// Check if there's a new service worker available and, if so, enqueue
// the update:
await registration.update();
try {
await registration.update();
} catch (e) {
// registration.update() throws if it can't reach the server.
// We're swallowing the error to keep the app working in offline mode
// or when playground.wordpress.net is down. We can be sure we have a functional
// service worker at this point because sw.register() succeeded.
logger.error('Failed to update service worker.', e);
}

// Proxy the service worker messages to the web worker:
navigator.serviceWorker.addEventListener(
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/blueprints/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"ignorePatterns": ["!**/*", "blueprint-schema-validator.js"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
Expand Down
25 changes: 23 additions & 2 deletions packages/playground/blueprints/bin/generate-schema.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import tsj from 'ts-json-schema-generator';
import fs from 'fs';
import Ajv from 'ajv';
import ajvStandaloneCode from 'ajv/dist/standalone/index.js';
import prettier from 'prettier';

/** @type {import('ts-json-schema-generator/dist/src/Config').Config} */
const config = {
Expand All @@ -11,6 +14,8 @@ const config = {

const output_path =
'packages/playground/blueprints/public/blueprint-schema.json';
const validator_output_path =
'packages/playground/blueprints/public/blueprint-schema-validator.js';

const maxRetries = 2;
async function exponentialBackoff(callback, retries = 0, delay = 1000) {
Expand Down Expand Up @@ -58,6 +63,22 @@ const schemaString = JSON.stringify(schema, null, 2)
// Naively remove TypeScript generics <T> from the schema:
.replaceAll(/%3C[a-zA-Z]+%3E/g, '')
.replaceAll(/<[a-zA-Z]+>/g, '');
fs.writeFile(output_path, schemaString, (err) => {
if (err) throw err;
fs.writeFileSync(output_path, schemaString);

const ajv = new Ajv({
discriminator: true,
code: {
source: true,
esm: true,
},
});
const validate = ajv.compile(schema);
const rawValidationCode = ajvStandaloneCode(ajv, validate);

// Use prettier to make the generated validation code more readable.
const prettierConfig = JSON.parse(fs.readFileSync('.prettierrc', 'utf8'));
const formattedValidationCode = prettier.format(
rawValidationCode,
prettierConfig
);
fs.writeFileSync(validator_output_path, formattedValidationCode);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import type { ValidateFunction } from 'ajv';
export default BlueprintValidateFunction as ValidateFunction;
Loading

0 comments on commit 95552a0

Please sign in to comment.