Skip to content

[PHP] Generate extension manifest validator - #3590

Closed
adamziel wants to merge 0 commit into
codex/spx-extension-assets-apifrom
codex/spx-extension-manifest-ajv-validator
Closed

[PHP] Generate extension manifest validator#3590
adamziel wants to merge 0 commit into
codex/spx-extension-assets-apifrom
codex/spx-extension-manifest-ajv-validator

Conversation

@adamziel

@adamziel adamziel commented May 1, 2026

Copy link
Copy Markdown
Collaborator

What

Stacks on #3580.

Replaces the handwritten extension manifest shape checks with a standalone AJV validator generated from the PHPExtensionManifest TypeScript type, following the same committed-validator pattern used by the Blueprints package.

Why

The manifest API is now more than a small object shape, and keeping validation logic handwritten would make the type, docs, schema, and runtime checks drift over time.

This keeps the type as the source of truth while retaining the explicit VFS path traversal guard as semantic validation.

Testing

  • npm exec nx -- run php-wasm-universal:build:extension-manifest-schema
  • 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
@adamziel
adamziel force-pushed the codex/spx-extension-assets-api branch from 51cfce5 to 290b2ce Compare May 1, 2026 23:59
@adamziel adamziel closed this May 2, 2026
@adamziel
adamziel force-pushed the codex/spx-extension-manifest-ajv-validator branch from 007948d to 786fe13 Compare May 2, 2026 02:12
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
\`\`\`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment