Skip to content

Add SPX profiler extension support - #3255

Closed
mdrovdahl wants to merge 1 commit into
WordPress:trunkfrom
mdrovdahl:add/spx-profiler-extension
Closed

Add SPX profiler extension support#3255
mdrovdahl wants to merge 1 commit into
WordPress:trunkfrom
mdrovdahl:add/spx-profiler-extension

Conversation

@mdrovdahl

@mdrovdahl mdrovdahl commented Feb 10, 2026

Copy link
Copy Markdown

Note- this is exploratory, all code was written by Claude and has not been reviewed by a human. Experiment is to make PHP-SPX run in Playground.

Summary

  • Add PHP-SPX as an optional profiler extension for the Playground CLI
  • SPX provides full tracing profiling with a built-in web UI (flame charts, icicle charts, function tables)
  • The WASM build is cross-compiled from the upstream C source using Emscripten as a side module — build infrastructure at mdrovdahl/spx-wasm

Usage

npx wp-playground server --php=8.2 --spx
# Profile a page: http://localhost:9400/?SPX_KEY=dev&SPX_ENABLED=1
# Profiler UI:    http://localhost:9400/?SPX_KEY=dev&SPX_UI_URI=/

What works

  • Full tracing profiler (wall time, Zend Engine memory metrics)
  • Flat profile and trace reporters
  • Built-in web UI served from the Emscripten VFS
  • Per-request report storage and data API

WASM limitations

  • Sampling profiler (requires pthreads, gracefully returns NULL)
  • RSS/IO tracking (no /proc filesystem)
  • CPU time (reports wall time — single-threaded WASM)

Changes

  • packages/php-wasm/node-builds/8-2/ — pre-built spx.so binary (72KB) and web UI assets for asyncify and jspi
  • packages/php-wasm/node/src/lib/extensions/spx/with-spx.ts (VFS loader) and get-spx-extension-module.ts (version resolver)
  • packages/php-wasm/node/src/lib/load-runtime.tswithSpx option in PHPLoaderOptions
  • packages/playground/cli/src/run-cli.ts--spx CLI flag
  • packages/playground/cli/src/blueprints-v1/* — pass withSpx through v1 workers
  • packages/playground/cli/src/blueprints-v2/* — pass withSpx through v2 workers

Test plan

  • npx nx dev playground-cli server --php=8.2 --spx --login starts without errors
  • Visiting /?SPX_KEY=dev&SPX_ENABLED=1 profiles the request
  • Visiting /?SPX_KEY=dev&SPX_UI_URI=/ shows the SPX web UI
  • Clicking a report in the UI renders the flame chart with correct timing values
  • Running without --spx is unaffected

Disclaimer

This work was produced with Claude Code (Claude Opus 4.6) and Gas Town. The codebase has not been fully reviewed by a human.

🤖 Generated with Claude Code

Add PHP-SPX (https://github.com/NoiseByNorthwest/php-spx) as an optional
profiler extension for the Playground CLI. SPX provides full tracing
profiling with a built-in web UI featuring flame charts, icicle charts,
and function tables.

The WASM build of SPX is cross-compiled from the upstream C source using
Emscripten as a side module. Source and build infrastructure:
https://github.com/mdrovdahl/spx-wasm

Usage:
  npx wp-playground server --php=8.2 --spx
  # Profile: http://localhost:9400/?SPX_KEY=dev&SPX_ENABLED=1
  # Web UI:  http://localhost:9400/?SPX_KEY=dev&SPX_UI_URI=/

What works:
- Full tracing profiler (wall time, Zend Engine memory metrics)
- Flat profile and trace reporters
- Built-in web UI served from the Emscripten VFS
- Per-request report storage and data API

WASM limitations:
- Sampling profiler (requires pthreads, gracefully returns NULL)
- RSS/IO tracking (no /proc filesystem)
- CPU time (reports wall time in single-threaded WASM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 10, 2026 17:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional PHP-SPX profiling support to the Playground CLI and php-wasm Node runtime, including a bundled SPX web UI served from the WASM VFS.

Changes:

  • Introduces a --spx CLI flag and threads withSpx through v1/v2 workers.
  • Extends php-wasm runtime loader options with withSpx and adds an SPX extension loader (module resolver + VFS/ini setup).
  • Adds prebuilt SPX artifacts for PHP 8.2 (binary + web UI assets for jspi/asyncify builds).

Reviewed changes

Copilot reviewed 31 out of 37 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/playground/cli/src/run-cli.ts Adds --spx flag to CLI option parsing.
packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts Threads withSpx through v2 worker boot args/options.
packages/playground/cli/src/blueprints-v2/blueprints-v2-handler.ts Enables withSpx based on CLI args for v2 workers.
packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts Threads withSpx into v1 worker runtime factory options.
packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts Enables withSpx based on CLI args for v1 workers.
packages/php-wasm/node/src/lib/load-runtime.ts Adds withSpx option and applies SPX extension setup during runtime load.
packages/php-wasm/node/src/lib/extensions/spx/with-spx.ts Implements SPX extension loading + ini + VFS web UI/data dir setup.
packages/php-wasm/node/src/lib/extensions/spx/get-spx-extension-module.ts Resolves SPX module artifacts by PHP version.
packages/php-wasm/node-builds/8-2/src/index.ts Exposes paths for SPX .so and web UI assets (asyncify/jspi).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/report.html Adds SPX report UI HTML (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/widget.js Adds SPX UI widgets (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/utils.js Adds SPX UI utilities (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/svg.js Adds SPX UI SVG helpers (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/profileData.js Adds SPX UI profile parsing/render model (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/math.js Adds SPX UI math helpers (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/layoutSplitter.js Adds SPX UI layout splitter logic (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/fmt.js Adds SPX UI formatting helpers (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/js/dataTable.js Adds SPX UI sortable table helper (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/index.html Adds SPX control panel index UI (vendored).
packages/php-wasm/node-builds/8-2/jspi/extensions/spx/8_2/web-ui/css/main.css Adds SPX UI styles (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/report.html Adds SPX report UI HTML (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/utils.js Adds SPX UI utilities (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/svg.js Adds SPX UI SVG helpers (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/profileData.js Adds SPX UI profile parsing/render model (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/math.js Adds SPX UI math helpers (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/layoutSplitter.js Adds SPX UI layout splitter logic (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/fmt.js Adds SPX UI formatting helpers (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/dataTable.js Adds SPX UI sortable table helper (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/index.html Adds SPX control panel index UI (vendored).
packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/css/main.css Adds SPX UI styles (vendored).
Comments suppressed due to low confidence (2)

packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/math.js:1

  • Vec3.bound() currently calls bound(...) but discards the returned values, so vector components are never clamped. This breaks callers like toHTMLColor() (especially after mult(1.5)), leading to invalid/out-of-range RGB values. Assign the results back, e.g. this.x = bound(this.x, low, up) (same for y/z).
    packages/php-wasm/node-builds/8-2/asyncify/extensions/spx/8_2/web-ui/js/profileData.js:1
  • Two issues: (1) metricsInfo is iterated as an array elsewhere, so Object.keys(this.metricsInfo) returns index strings (e.g. '0', '1'), not metric keys—this should likely return this.metricsInfo.map((i) => i.key). (2) The thrown error references an undefined variable key; it should use metric so the error message is correct.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +822 to +825
const pushTarget = Math.max(idx - 1, 0);
switch (elem.name) {
case 'push-down':
pushTarget = Math.min(idx + 1, categories.length - 1);

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushTarget is declared as const but then reassigned in the 'push-down' case, which will throw at runtime. Change it to let pushTarget = ...; if the fallthrough from 'push-down' to 'push-up' is intentional, consider adding an explicit comment to avoid it being “fixed” later, or add a break and duplicate the splice logic.

Suggested change
const pushTarget = Math.max(idx - 1, 0);
switch (elem.name) {
case 'push-down':
pushTarget = Math.min(idx + 1, categories.length - 1);
let pushTarget = Math.max(idx - 1, 0);
switch (elem.name) {
case 'push-down':
pushTarget = Math.min(idx + 1, categories.length - 1);
// fall through to 'push-up' to apply splice with updated pushTarget
Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is vendored upstream SPX code from NoiseByNorthwest/php-spx. These files are copied as-is from the SPX build output — not ours to modify here.

Comment on lines +54 to +56
bound(this.x, low, up);
bound(this.y, low, up);
bound(this.z, low, up);

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vec3.bound() currently calls bound(...) but discards the returned values, so vector components are never clamped. This breaks callers like toHTMLColor() (especially after mult(1.5)), leading to invalid/out-of-range RGB values. Assign the results back, e.g. this.x = bound(this.x, low, up) (same for y/z).

Suggested change
bound(this.x, low, up);
bound(this.y, low, up);
bound(this.z, low, up);
this.x = bound(this.x, low, up);
this.y = bound(this.y, low, up);
this.z = bound(this.z, low, up);
Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is vendored upstream SPX code from NoiseByNorthwest/php-spx. These files are copied as-is from the SPX build output and are not ours to modify. Any fixes should go upstream to the php-spx project.

Comment on lines +1197 to +1209
getMetricKeys() {
return Object.keys(this.metricsInfo);
}

getMetricInfo(metric) {
for (let info of this.metricsInfo) {
if (info.key == metric) {
return info;
}
}

throw new Error('Unknown metric: ' + key);
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues: (1) metricsInfo is iterated as an array elsewhere, so Object.keys(this.metricsInfo) returns index strings (e.g. '0', '1'), not metric keys—this should likely return this.metricsInfo.map((i) => i.key). (2) The thrown error references an undefined variable key; it should use metric so the error message is correct.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is vendored upstream SPX code from NoiseByNorthwest/php-spx. These files are copied as-is from the SPX build output and are not ours to modify. Any fixes should go upstream to the php-spx project.

Comment on lines +42 to +46
return {
...options,
ENV: {
...options.ENV,
PHP_INI_SCAN_DIR: '/internal/shared/extensions',

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting PHP_INI_SCAN_DIR to a fixed path can clobber an existing scan dir from options.ENV (and any upstream defaults), potentially preventing other .ini files from being discovered. Prefer appending the SPX extension ini directory to the existing PHP_INI_SCAN_DIR value (using : as PHP’s separator) rather than replacing it.

Suggested change
return {
...options,
ENV: {
...options.ENV,
PHP_INI_SCAN_DIR: '/internal/shared/extensions',
const spxIniDir = '/internal/shared/extensions';
const existingScanDir =
options.ENV && typeof options.ENV.PHP_INI_SCAN_DIR === 'string'
? options.ENV.PHP_INI_SCAN_DIR
: undefined;
return {
...options,
ENV: {
...options.ENV,
PHP_INI_SCAN_DIR: existingScanDir
? `${existingScanDir}:${spxIniDir}`
: spxIniDir,
Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern used by all other Playground extensions — withXdebug, withRedis, withMemcached, and withIntl all set PHP_INI_SCAN_DIR to the same fixed path (/internal/shared/extensions) in the same way. See for example with-xdebug.ts. This is the established convention in the codebase for extension loading.

Comment on lines +79 to +86
[
'extension=/internal/shared/extensions/spx.so',
`spx.data_dir=${SPX_VFS_DATA_DIR}`,
'spx.http_enabled=1',
'spx.http_key=dev',
'spx.http_ip_whitelist=*',
`spx.http_ui_assets_dir=${SPX_VFS_WEB_UI_DIR}`,
].join('\n')

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default ini enables the HTTP UI with a fixed key (dev) and an unrestricted whitelist (*). If the Playground server is bound to a non-loopback interface, this exposes profiling data/UI to the local network by default when --spx is used. Consider defaulting spx.http_ip_whitelist to loopback only (e.g. 127.0.0.1) and generating a per-run random spx.http_key (or allowing it to be configured via CLI/env) to reduce exposure.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a local development profiling tool, gated behind the --spx CLI flag (off by default). The spx.http_key=dev and spx.http_ip_whitelist=* settings follow SPX's own defaults for development use. The Playground CLI server binds to 127.0.0.1 by default. These settings are consistent with a dev-only profiler — there's no production deployment path for this.

Comment on lines +14 to +31
function copyDirToVFS(phpRuntime: PHPRuntime, hostDir: string, vfsDir: string) {
if (!FSHelpers.fileExists(phpRuntime.FS, vfsDir)) {
phpRuntime.FS.mkdirTree(vfsDir);
}
for (const entry of fs.readdirSync(hostDir, { withFileTypes: true })) {
const hostPath = path.join(hostDir, entry.name);
const vfsPath = `${vfsDir}/${entry.name}`;
if (entry.isDirectory()) {
copyDirToVFS(phpRuntime, hostPath, vfsPath);
} else if (entry.isFile()) {
if (!FSHelpers.fileExists(phpRuntime.FS, vfsPath)) {
phpRuntime.FS.writeFile(
vfsPath,
new Uint8Array(fs.readFileSync(hostPath))
);
}
}
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses synchronous, recursive filesystem reads during onRuntimeInitialized, which can block the event loop on startup (especially if the UI asset directory grows). Consider switching to an async copy (preload assets earlier), batching large reads, or packaging assets into a single archive to reduce FS overhead during initialization.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern used by all other Playground extensions. See with-xdebug.ts, with-redis.ts, and with-memcached.ts — they all use synchronous reads in onRuntimeInitialized to load extension .so files and assets into the Emscripten VFS. The callback is synchronous by design.

Comment on lines +11 to +21
switch (version) {
case '8.2': {
// @ts-ignore
const mod = await import('@php-wasm/node-8-2');
return {
extensionPath: await mod.getSpxExtensionPath(),
webUiPath: await mod.getSpxWebUiPath(),
};
}
}
throw new Error(`SPX extension not available for PHP ${version}`);

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using // @ts-ignore for the dynamic import makes it easy to miss genuine typing issues and diverges from the pattern used elsewhere (e.g. the runtime loader module resolver). Prefer adding proper module typings (or a PHPLoaderModule-style interface) and importing with an explicit type annotation instead of suppressing TypeScript.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern used by all other extension module resolvers in the codebase. See get-xdebug-extension-module.ts, get-redis-extension-module.ts, etc. — they all use // @ts-ignore on the dynamic import because the path is resolved at build time from the node-builds package.

Comment on lines +282 to +286
spx: {
describe: 'Enable SPX profiler.',
type: 'boolean',
default: false,
},

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spx option definition is duplicated in two option blocks. To avoid future drift (e.g. different description/default), consider defining it once (similar to other shared options) and referencing it in both places.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern used for all other extension CLI options in this file. The xdebug, intl, redis, and memcached options are also duplicated across the sharedOptions and serverOnlyOptions blocks. If this should be refactored, it would be a broader cleanup across all extensions, not specific to SPX.

Comment on lines +382 to +386
spx: {
describe: 'Enable SPX profiler.',
type: 'boolean',
default: false,
},

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spx option definition is duplicated in two option blocks. To avoid future drift (e.g. different description/default), consider defining it once (similar to other shared options) and referencing it in both places.

Suggested change
spx: {
describe: 'Enable SPX profiler.',
type: 'boolean',
default: false,
},
spx: sharedOptions.spx,
Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — this duplication mirrors the existing pattern for all other extension options (xdebug, intl, redis, memcached) in this file.

@mho22 mho22 self-assigned this Apr 15, 2026
@mho22

mho22 commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

@mdrovdahl There's a new pull request in review here which let you load your custom PHP.wasm extension. It is specifically built for use cases like yours. We still need to check if it works for your use case with extra files but I think you can give it a try when it is merged.

By the way, thank you for your work! It helped us investigate in external custom extensions support.

adamziel added a commit that referenced this pull request May 3, 2026
## What it does

Adds manifest-declared sidecar files to the PHP extension loading API
introduced in #3566, and folds in #3590's generated AJV validator for
extension manifests.

A manifest can now describe files and directories that must be staged
before PHP starts, alongside the selected `.so` artifact. Each node's
final VFS path is `vfsRoot + vfsPath`:

```json
{
  "name": "spx",
  "artifacts": [
    {
      "phpVersion": "8.4",
      "sourcePath": "spx.so",
      "extraFiles": {
        "vfsRoot": "/internal/shared/spx",
        "nodes": [
          { "vfsPath": "data", "type": "directory" },
          { "vfsPath": "ui/index.html", "sourcePath": "ui/index.html" }
        ]
      }
    }
  ]
}
```

## Rationale

#3255 needs SPX to load more than a WASM side module. The extension also
expects a writable data directory and web UI assets such as HTML, CSS,
and JS files.

This PR keeps that work focused on the loader contract. It does **not**
rebuild PHP 8.2, ship SPX artifacts, or claim that SPX loads today. A
follow-up can use this manifest shape when adding or rebuilding SPX
artifacts.

## Integration

For most consumers nothing changes structurally — manifest sources slot
into the same `extensions` array as bundled extension names:

```ts
await loadNodeRuntime('8.4', {
  extensions: [
    'intl',
    'xdebug',
    { source: { format: 'manifest', manifestUrl: 'https://example.com/spx/manifest.json' } },
  ],
});
```

For hand-rolled Emscripten setups, `resolvePHPExtension` +
`withResolvedPHPExtensions` in `@php-wasm/universal` are the lower-level
pair.

## Implementation

`PHPExtensionManifest` accepts `extraFiles` at two levels:

- manifest-level `extraFiles` for files shared by every artifact
- artifact-level `extraFiles` for PHP-version-specific files

Each group has a `vfsRoot` (the absolute VFS prefix) and an array of
`nodes` with a `vfsPath`, an optional `type: 'file' | 'directory'`, and
a `sourcePath` (the URL to fetch, only required for files). The resolver
joins `vfsRoot + vfsPath` into absolute VFS paths and returns:

```ts
{
  files: { '/internal/shared/spx/ui/index.html': bytes },
  directories: ['/internal/shared/spx/data'],
}
```

The resolver validates untrusted manifest JSON with a standalone AJV
validator generated from the public `PHPExtensionManifest` TypeScript
type. Sidecar files are fetched relative to the manifest URL or inline
manifest `baseUrl`, with manifest-level and selected artifact-level
nodes fetched in parallel through a shared concurrency limit.

Caller-supplied `extraFiles` on `PHPExtensionInstallOptions` use the
same absolute-path shape (`{ files, directories? }`) — no `targetPath`
indirection.

External extension manifests remain JSPI-only.

## Breaking changes

- Manifest field renames: `artifact.file` → `artifact.sourcePath`;
`extraFiles.targetPath` → `vfsRoot`;
`extraFiles.files`/`extraFiles.directories` → unified `nodes` array with
`vfsPath`/`type`/`sourcePath`.
- `PHPExtensionExtraFiles` (caller-supplied sidecar files) is now flat
absolute VFS paths under `files` and `directories`. The previous
`targetPath + relative paths` shape is gone.
- `sha256` fields removed from sources and artifacts.
- `ResolvePHPExtensionOptions` is no longer exported (use
`PHPExtensionInstallOptions & { phpVersion: string }`).

## Testing instructions

\`\`\`bash
npm exec nx -- test php-wasm-universal --testFile=load-extension.spec.ts
npm exec nx -- typecheck php-wasm-universal
npm exec nx -- lint php-wasm-universal
npm exec nx -- build php-wasm-universal
cd packages/php-wasm/node && npx vitest run
src/test/with-php-extensions.spec.ts --config vite.jspi.config.ts
npm exec nx -- typecheck php-wasm-node
npm exec nx -- lint php-wasm-node
npm exec nx -- typecheck php-wasm-web
\`\`\`
@mho22

mho22 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Closing this pull request now that the custom PHP extensions feature has landed.

@mho22 mho22 closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants