From c2e504edd8f9789f3ea8efe9e84a3735e4f1f3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 12 May 2026 00:23:11 +0200 Subject: [PATCH 1/3] Add CLI support for custom PHP extensions --- packages/playground/cli/README.md | 39 ++++++- .../blueprints-v1/blueprints-v1-handler.ts | 4 +- .../blueprints-v2/blueprints-v2-handler.ts | 4 +- packages/playground/cli/src/php-extensions.ts | 110 ++++++++++++++++-- packages/playground/cli/src/run-cli.ts | 18 +++ .../cli/tests/php-extensions.spec.ts | 96 +++++++++++++++ 6 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 packages/playground/cli/tests/php-extensions.spec.ts diff --git a/packages/playground/cli/README.md b/packages/playground/cli/README.md index 4338c15c5e3..bf9c085626c 100644 --- a/packages/playground/cli/README.md +++ b/packages/playground/cli/README.md @@ -94,13 +94,50 @@ The `server` command supports the following optional arguments: - `--wordpress-install-mode `: Control how Playground prepares WordPress before booting. Defaults to `download-and-install`. Other options: `install-from-existing-files` (install using files you've mounted), `install-from-existing-files-if-needed` (same, but skip setup when an existing site is detected), and `do-not-attempt-installing` (never download or install WordPress). - `--skip-sqlite-setup`: Do not set up the SQLite database integration. - `--verbosity`: Output logs and progress messages (choices: "quiet", "normal", "debug"). Defaults to "normal". - - `--debug`: Print the PHP error log if an error occurs during boot. - `--follow-symlinks`: Allow Playground to follow symlinks by automatically mounting symlinked directories and files encountered in mounted directories. ⚠️ Warning: Following symlinks will expose files outside mounted directories to Playground and could be a security risk. - `--workers=`: Number of request-handling worker threads. Pass a positive integer, or `auto` to use one worker per CPU core (minus one). Defaults to `min(6, cpus-1)`. Useful for multi-client workloads (e.g. parallel e2e suites) that need more than 6 in-flight requests. - `--experimental-multi-worker`: Deprecated. Use `--workers=` instead. The value of this flag is ignored. - `--phpmyadmin[=]`: Install phpMyAdmin for database management. The phpMyAdmin URL will be printed after boot. Optionally specify a custom URL path (default: `/phpmyadmin`). - `--internal-cookie-store`: Enables Playground's internal cookie handling. When active, Playground uses an HttpCookieStore to manage and persist cookies across requests. If disabled, cookies are handled externally, like by a browser in Node.js. +- `--php-extension=`: Load a custom PHP.wasm extension manifest before PHP starts. Accepts local paths, `file:` URLs, and `http(s):` URLs. Can be used multiple times. +- `--php-extension-config=`: Load a JSON extension config before PHP starts. Use this for direct `.so` URLs or extension-specific `iniEntries` and `env` settings. Can be used multiple times. + +### Loading Custom PHP.wasm Extensions + +Custom extensions built with `@php-wasm/compile-extension` can be loaded with +`--php-extension`: + +```bash +npx @wp-playground/cli@latest server \ + --php=8.4 \ + --php-extension=./dist/wp_mysql_parser/manifest.json +``` + +The manifest selects the `.so` artifact matching the active PHP version and can +stage sidecar files before PHP starts. External extensions are JSPI-only, so use +Node.js 23 or newer. + +Use `--php-extension-config` when the extension needs more runtime settings: + +```json +{ + "source": { + "format": "manifest", + "manifestUrl": "./dist/spx/manifest.json" + }, + "iniEntries": { + "spx.http_enabled": "1" + }, + "env": { + "SPX_DATA_DIR": "/internal/shared/spx/data" + } +} +``` + +```bash +npx @wp-playground/cli@latest server --php-extension-config=./spx.json +``` ## Need some help with the CLI? diff --git a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts index 6d7aced0ef0..7df03774f52 100644 --- a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts +++ b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts @@ -32,7 +32,7 @@ import { mergeDefinedConstants, } from '../run-cli'; import type { CLIOutput } from '../cli-output'; -import { legacyPHPExtensionsObjectToExtensionsArray } from '../php-extensions'; +import { cliExtensionArgsToExtensionsArray } from '../php-extensions'; /** * Boots Playground CLI workers using Blueprint version 1. @@ -198,7 +198,7 @@ export class BlueprintsV1Handler { processId: worker.processId, followSymlinks: this.args.followSymlinks === true, trace: this.args.experimentalTrace === true, - extensions: legacyPHPExtensionsObjectToExtensionsArray(this.args), + extensions: cliExtensionArgsToExtensionsArray(this.args), nativeInternalDirPath, pathAliases: this.args.pathAliases, }); diff --git a/packages/playground/cli/src/blueprints-v2/blueprints-v2-handler.ts b/packages/playground/cli/src/blueprints-v2/blueprints-v2-handler.ts index bf0cc058961..1f098c80619 100644 --- a/packages/playground/cli/src/blueprints-v2/blueprints-v2-handler.ts +++ b/packages/playground/cli/src/blueprints-v2/blueprints-v2-handler.ts @@ -13,7 +13,7 @@ import { mergeDefinedConstants, } from '../run-cli'; import type { CLIOutput } from '../cli-output'; -import { legacyPHPExtensionsObjectToExtensionsArray } from '../php-extensions'; +import { cliExtensionArgsToExtensionsArray } from '../php-extensions'; /** * Boots Playground CLI workers using Blueprint version 2. @@ -83,7 +83,7 @@ export class BlueprintsV2Handler { siteUrl: this.siteUrl, processId: worker.processId, trace: this.args.verbosity === 'debug', - extensions: legacyPHPExtensionsObjectToExtensionsArray(this.args), + extensions: cliExtensionArgsToExtensionsArray(this.args), nativeInternalDirPath, mountsBeforeWpInstall: this.args['mount-before-install'] || [], mountsAfterWpInstall: this.args.mount || [], diff --git a/packages/playground/cli/src/php-extensions.ts b/packages/playground/cli/src/php-extensions.ts index 10ce45638b5..6c915419f90 100644 --- a/packages/playground/cli/src/php-extensions.ts +++ b/packages/playground/cli/src/php-extensions.ts @@ -1,24 +1,32 @@ -import type { PHPExtension, XdebugOptions } from '@php-wasm/node'; +import { readFileSync } from 'node:fs'; +import type { + PHPExtension, + RuntimePHPExtensionSource, + XdebugOptions, +} from '@php-wasm/node'; /** - * Converts the legacy Playground CLI extension options object into the runtime - * `extensions` array. + * Converts Playground CLI extension options into the runtime `extensions` + * array. * - * The CLI still receives extensions as individual options: `intl`, `redis`, - * `memcached`, and `xdebug`. The PHP runtime no longer has separate `with*` - * entry points for new callers; it expects one array that can contain built-in - * extension names and, elsewhere, external extension sources. This function is - * the CLI boundary between those two shapes. + * The CLI receives built-in extensions as individual options (`intl`, `redis`, + * `memcached`, and `xdebug`) and external extensions as manifest/config paths. + * The PHP runtime expects one array that can contain built-in names and + * external extension sources side by side. * * Xdebug is the only CLI extension here with options. A plain `true` becomes * the built-in `xdebug` request, while an object preserves the Xdebug settings * and passes them through to the Node runtime. */ -export function legacyPHPExtensionsObjectToExtensionsArray(args: { +export function cliExtensionArgsToExtensionsArray(args: { intl?: boolean; redis?: boolean; memcached?: boolean; xdebug?: boolean | XdebugOptions; + phpExtension?: string[]; + 'php-extension'?: string[]; + phpExtensionConfig?: string[]; + 'php-extension-config'?: string[]; }): PHPExtension[] { const extensions: PHPExtension[] = []; if (args.intl) { @@ -37,5 +45,89 @@ export function legacyPHPExtensionsObjectToExtensionsArray(args: { : 'xdebug' ); } + for (const manifestUrl of getArrayOption(args, 'phpExtension')) { + extensions.push({ + source: { + format: 'manifest', + manifestUrl, + }, + }); + } + for (const configPath of getArrayOption(args, 'phpExtensionConfig')) { + extensions.push(readPHPExtensionConfig(configPath)); + } return extensions; } + +export function readPHPExtensionConfig( + configPath: string +): RuntimePHPExtensionSource { + let config: unknown; + try { + config = JSON.parse(readFileSync(configPath, 'utf8')); + } catch (error) { + throw new Error(`Could not read PHP extension config: ${configPath}`, { + cause: error, + }); + } + + if (!isRecord(config) || !isRecord(config['source'])) { + throw new Error( + `Invalid PHP extension config: ${configPath}. Expected an object with a source field.` + ); + } + + const source = config['source']; + if (source['format'] === 'so') { + throw new Error( + `Invalid PHP extension config: ${configPath}. The CLI cannot load direct bytes; use a manifest or URL source.` + ); + } + if (source['format'] === 'url') { + if (typeof source['url'] !== 'string') { + throw new Error( + `Invalid PHP extension config: ${configPath}. A URL source requires a string url.` + ); + } + return config as RuntimePHPExtensionSource; + } + if (source['format'] === 'manifest') { + if ( + typeof source['manifestUrl'] !== 'string' && + !isRecord(source['manifest']) + ) { + throw new Error( + `Invalid PHP extension config: ${configPath}. A manifest source requires manifestUrl or manifest.` + ); + } + return config as RuntimePHPExtensionSource; + } + + throw new Error( + `Invalid PHP extension config: ${configPath}. Unknown source format.` + ); +} + +function getArrayOption( + args: { + phpExtension?: string[]; + 'php-extension'?: string[]; + phpExtensionConfig?: string[]; + 'php-extension-config'?: string[]; + }, + camelCaseKey: 'phpExtension' | 'phpExtensionConfig' +): string[] { + const dashCaseKey = + camelCaseKey === 'phpExtension' + ? 'php-extension' + : 'php-extension-config'; + const value = args[camelCaseKey] ?? args[dashCaseKey]; + if (value === undefined) { + return []; + } + return Array.isArray(value) ? value : [value]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 36e91caf184..31de2ad2134 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -299,6 +299,20 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { type: 'boolean', default: false, }, + 'php-extension': { + describe: + 'Load a custom PHP.wasm extension manifest before PHP starts. Can be a local path, file: URL, or http(s) URL. Can be used multiple times.', + type: 'array', + string: true, + nargs: 1, + }, + 'php-extension-config': { + describe: + 'Load a JSON PHP.wasm extension config before PHP starts. Use this for direct .so URLs or extension-specific ini/env settings. Can be used multiple times.', + type: 'array', + string: true, + nargs: 1, + }, 'experimental-unsafe-ide-integration': { describe: 'Enable experimental IDE development tools. This option edits IDE config files ' + @@ -422,6 +436,8 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { type: 'boolean', default: false, }, + 'php-extension': sharedOptions['php-extension'], + 'php-extension-config': sharedOptions['php-extension-config'], 'experimental-unsafe-ide-integration': sharedOptions['experimental-unsafe-ide-integration'], 'skip-browser': { @@ -896,6 +912,8 @@ export interface RunCLIArgs { redis?: boolean; memcached?: boolean; xdebug?: boolean | XdebugOptions; + phpExtension?: string[]; + phpExtensionConfig?: string[]; experimentalUnsafeIdeIntegration?: string[]; experimentalDevtools?: boolean; 'experimental-blueprints-v2-runner'?: boolean; diff --git a/packages/playground/cli/tests/php-extensions.spec.ts b/packages/playground/cli/tests/php-extensions.spec.ts new file mode 100644 index 00000000000..35208b7aca5 --- /dev/null +++ b/packages/playground/cli/tests/php-extensions.spec.ts @@ -0,0 +1,96 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import { + cliExtensionArgsToExtensionsArray, + readPHPExtensionConfig, +} from '../src/php-extensions'; + +describe('CLI PHP extensions', () => { + test('converts built-in extension flags to runtime extension requests', () => { + expect( + cliExtensionArgsToExtensionsArray({ + intl: true, + redis: true, + memcached: true, + xdebug: true, + }) + ).toEqual(['intl', 'redis', 'memcached', 'xdebug']); + }); + + test('converts --php-extension values to manifest extension requests', () => { + expect( + cliExtensionArgsToExtensionsArray({ + phpExtension: [ + './dist/wp_mysql_parser/manifest.json', + 'https://example.com/spx/manifest.json', + ], + }) + ).toEqual([ + { + source: { + format: 'manifest', + manifestUrl: './dist/wp_mysql_parser/manifest.json', + }, + }, + { + source: { + format: 'manifest', + manifestUrl: 'https://example.com/spx/manifest.json', + }, + }, + ]); + }); + + test('reads --php-extension-config JSON files', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); + const configPath = path.join(tempDir, 'extension.json'); + await writeFile( + configPath, + JSON.stringify({ + name: 'wp_mysql_parser', + source: { + format: 'url', + url: './dist/wp_mysql_parser-php8.4-jspi.so', + }, + iniEntries: { + 'wp_mysql_parser.mode': 'parser', + }, + }) + ); + + try { + expect(readPHPExtensionConfig(configPath)).toEqual({ + name: 'wp_mysql_parser', + source: { + format: 'url', + url: './dist/wp_mysql_parser-php8.4-jspi.so', + }, + iniEntries: { + 'wp_mysql_parser.mode': 'parser', + }, + }); + expect( + cliExtensionArgsToExtensionsArray({ + phpExtensionConfig: [configPath], + }) + ).toEqual([readPHPExtensionConfig(configPath)]); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + test('rejects config files without an external extension source', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); + const configPath = path.join(tempDir, 'extension.json'); + await writeFile(configPath, JSON.stringify({ name: 'broken' })); + + try { + expect(() => readPHPExtensionConfig(configPath)).toThrow( + 'Expected an object with a source field' + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); +}); From 2126341dea4dc2ee338fe5e3fa099317843f11de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 14 May 2026 14:46:53 +0200 Subject: [PATCH 2/3] Simplify CLI PHP extension args --- packages/playground/cli/src/php-extensions.ts | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/playground/cli/src/php-extensions.ts b/packages/playground/cli/src/php-extensions.ts index 6c915419f90..f0fb9c60bc4 100644 --- a/packages/playground/cli/src/php-extensions.ts +++ b/packages/playground/cli/src/php-extensions.ts @@ -24,9 +24,7 @@ export function cliExtensionArgsToExtensionsArray(args: { memcached?: boolean; xdebug?: boolean | XdebugOptions; phpExtension?: string[]; - 'php-extension'?: string[]; phpExtensionConfig?: string[]; - 'php-extension-config'?: string[]; }): PHPExtension[] { const extensions: PHPExtension[] = []; if (args.intl) { @@ -45,7 +43,7 @@ export function cliExtensionArgsToExtensionsArray(args: { : 'xdebug' ); } - for (const manifestUrl of getArrayOption(args, 'phpExtension')) { + for (const manifestUrl of args.phpExtension || []) { extensions.push({ source: { format: 'manifest', @@ -53,7 +51,7 @@ export function cliExtensionArgsToExtensionsArray(args: { }, }); } - for (const configPath of getArrayOption(args, 'phpExtensionConfig')) { + for (const configPath of args.phpExtensionConfig || []) { extensions.push(readPHPExtensionConfig(configPath)); } return extensions; @@ -108,26 +106,6 @@ export function readPHPExtensionConfig( ); } -function getArrayOption( - args: { - phpExtension?: string[]; - 'php-extension'?: string[]; - phpExtensionConfig?: string[]; - 'php-extension-config'?: string[]; - }, - camelCaseKey: 'phpExtension' | 'phpExtensionConfig' -): string[] { - const dashCaseKey = - camelCaseKey === 'phpExtension' - ? 'php-extension' - : 'php-extension-config'; - const value = args[camelCaseKey] ?? args[dashCaseKey]; - if (value === undefined) { - return []; - } - return Array.isArray(value) ? value : [value]; -} - function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } From c1611eb08260885eadb168ca43e36287fdbfd840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 14 May 2026 15:00:16 +0200 Subject: [PATCH 3/3] Use PHP extension manifests for runtime config --- packages/php-wasm/compile-extension/README.md | 33 +- .../compile-extension/src/manifest.ts | 14 + ...php-extension-manifest-schema-validator.js | 2150 ++++++++++++----- .../public/php-extension-manifest-schema.json | 101 +- .../universal/src/lib/load-extension.spec.ts | 36 + .../universal/src/lib/load-extension.ts | 43 +- packages/playground/cli/README.md | 19 +- packages/playground/cli/src/php-extensions.ts | 66 +- packages/playground/cli/src/run-cli.ts | 9 - .../cli/tests/php-extensions.spec.ts | 60 +- 10 files changed, 1718 insertions(+), 813 deletions(-) diff --git a/packages/php-wasm/compile-extension/README.md b/packages/php-wasm/compile-extension/README.md index c3e21044817..ba00e2af427 100644 --- a/packages/php-wasm/compile-extension/README.md +++ b/packages/php-wasm/compile-extension/README.md @@ -44,6 +44,27 @@ npx @php-wasm/compile-extension \ Empty directories are recorded as `type: "directory"` nodes so the loader creates them before PHP starts. +If an extension needs startup settings, add them to the manifest: + +```json +{ + "name": "spx", + "version": "0.1.0", + "artifacts": [ + { + "phpVersion": "8.4", + "sourcePath": "spx-php8.4-jspi.so" + } + ], + "iniEntries": { + "spx.http_enabled": "1" + }, + "env": { + "SPX_DATA_DIR": "/internal/shared/spx/data" + } +} +``` + The supported `--php-versions` are `7.4` and `8.0` through `8.5`. Docker is required. The CLI lazily fetches the small PHP.wasm Docker asset set @@ -60,14 +81,14 @@ The package only needs Docker and Node. It does not require a checkout of - uses: actions/setup-node@v4 with: - node-version: '24' + node-version: '24' - run: | - npx --yes @php-wasm/compile-extension \ - --source ./my-extension \ - --name my_extension \ - --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \ - --out ./dist/my-extension + npx --yes @php-wasm/compile-extension \ + --source ./my-extension \ + --name my_extension \ + --php-versions 8.0,8.1,8.2,8.3,8.4,8.5 \ + --out ./dist/my-extension ``` In a matrix workflow, set `strategy.max-parallel: 1` on the WASM job — diff --git a/packages/php-wasm/compile-extension/src/manifest.ts b/packages/php-wasm/compile-extension/src/manifest.ts index 23fee83ccc6..78b2273a98f 100644 --- a/packages/php-wasm/compile-extension/src/manifest.ts +++ b/packages/php-wasm/compile-extension/src/manifest.ts @@ -35,6 +35,20 @@ export interface ExtensionArtifact { export interface ExtensionManifest { name: string; version: string; + /** + * The first directive of the generated startup `.ini` file. Defaults to + * `extension`; use `zend_extension` for Zend extensions like Xdebug. + */ + loadWithIniDirective?: 'extension' | 'zend_extension'; + /** Additional `key=value` lines for the generated startup `.ini` file. */ + iniEntries?: Record; + /** Environment variables added before the extension is loaded. */ + env?: Record; + /** + * VFS directory where PHP.wasm writes the extension `.so` file and its + * per-extension ini file. + */ + extensionDir?: string; artifacts: ExtensionArtifact[]; /** URL-backed files shared by every artifact in this manifest. */ extraFiles?: ExtensionManifestExtraFiles; diff --git a/packages/php-wasm/universal/public/php-extension-manifest-schema-validator.js b/packages/php-wasm/universal/public/php-extension-manifest-schema-validator.js index a93e210dd22..a2963e2b517 100644 --- a/packages/php-wasm/universal/public/php-extension-manifest-schema-validator.js +++ b/packages/php-wasm/universal/public/php-extension-manifest-schema-validator.js @@ -11,10 +11,51 @@ const schema11 = { name: { type: 'string' }, version: { type: 'string' }, mode: { type: 'string', const: 'php-extension' }, + loadWithIniDirective: { + $ref: '#/definitions/PHPExtensionIniDirective', + description: + 'The first directive of the generated startup `.ini` file. Defaults to `extension`; use `zend_extension` for Zend extensions like Xdebug.', + }, + iniEntries: { + type: 'object', + additionalProperties: { type: 'string' }, + description: + 'Additional `key=value` lines for the generated startup `.ini` file.', + }, + env: { + type: 'object', + additionalProperties: { type: 'string' }, + description: + 'Environment variables added before the extension is loaded.', + }, + extensionDir: { + type: 'string', + description: + 'VFS directory where PHP.wasm writes the extension `.so` file and its per-extension ini file. Defaults to `PHP_EXTENSIONS_DIR`.', + }, artifacts: { type: 'array', items: { - $ref: '#/definitions/PHPExtensionManifestArtifact', + type: 'object', + properties: { + phpVersion: { + type: 'string', + description: + 'PHP major/minor version, e.g. `8.4`.', + }, + sourcePath: { + type: 'string', + description: + 'Relative to the manifest URL/base URL, or an absolute URL.', + }, + extraFiles: { + $ref: '#/definitions/PHPExtensionManifestExtraFiles', + description: + 'URL-backed files needed only by this artifact.', + }, + }, + required: ['phpVersion', 'sourcePath'], + additionalProperties: false, }, }, extraFiles: { @@ -28,26 +69,11 @@ const schema11 = { description: 'Extension artifact manifest. Lets callers publish a matrix of `.so` files and lets `resolvePHPExtension()` select the artifact matching the current PHP version. External extension artifacts are JSPI-only.', }, - PHPExtensionManifestArtifact: { - type: 'object', - properties: { - phpVersion: { - type: 'string', - description: 'PHP major/minor version, e.g. `8.4`.', - }, - sourcePath: { - type: 'string', - description: - 'Relative to the manifest URL/base URL, or an absolute URL.', - }, - extraFiles: { - $ref: '#/definitions/PHPExtensionManifestExtraFiles', - description: - 'URL-backed files needed only by this artifact.', - }, - }, - required: ['phpVersion', 'sourcePath'], - additionalProperties: false, + PHPExtensionIniDirective: { + type: 'string', + enum: ['extension', 'zend_extension'], + description: + 'The php.ini directive used to load the extension. Use `extension` for regular PHP extensions and `zend_extension` for Zend extensions like Xdebug.', }, PHPExtensionManifestExtraFiles: { type: 'object', @@ -60,35 +86,32 @@ const schema11 = { nodes: { type: 'array', items: { - $ref: '#/definitions/PHPExtensionManifestExtraFile', + type: 'object', + properties: { + vfsPath: { + type: 'string', + description: + "Joined with the group's `vfsRoot` to form the final VFS path.", + }, + type: { + type: 'string', + enum: ['file', 'directory'], + description: + 'Defaults to "file". Only file nodes need a `sourcePath`.', + }, + sourcePath: { + type: 'string', + description: + 'Relative to the manifest URL/base URL, or an absolute URL.', + }, + }, + required: ['vfsPath'], + additionalProperties: false, }, }, }, additionalProperties: false, }, - PHPExtensionManifestExtraFile: { - type: 'object', - properties: { - vfsPath: { - type: 'string', - description: - "Joined with the group's `vfsRoot` to form the final VFS path.", - }, - type: { - type: 'string', - enum: ['file', 'directory'], - description: - 'Defaults to "file". Only file nodes need a `sourcePath`.', - }, - sourcePath: { - type: 'string', - description: - 'Relative to the manifest URL/base URL, or an absolute URL.', - }, - }, - required: ['vfsPath'], - additionalProperties: false, - }, }, }; const schema12 = { @@ -97,9 +120,51 @@ const schema12 = { name: { type: 'string' }, version: { type: 'string' }, mode: { type: 'string', const: 'php-extension' }, + loadWithIniDirective: { + $ref: '#/definitions/PHPExtensionIniDirective', + description: + 'The first directive of the generated startup `.ini` file. Defaults to `extension`; use `zend_extension` for Zend extensions like Xdebug.', + }, + iniEntries: { + type: 'object', + additionalProperties: { type: 'string' }, + description: + 'Additional `key=value` lines for the generated startup `.ini` file.', + }, + env: { + type: 'object', + additionalProperties: { type: 'string' }, + description: + 'Environment variables added before the extension is loaded.', + }, + extensionDir: { + type: 'string', + description: + 'VFS directory where PHP.wasm writes the extension `.so` file and its per-extension ini file. Defaults to `PHP_EXTENSIONS_DIR`.', + }, artifacts: { type: 'array', - items: { $ref: '#/definitions/PHPExtensionManifestArtifact' }, + items: { + type: 'object', + properties: { + phpVersion: { + type: 'string', + description: 'PHP major/minor version, e.g. `8.4`.', + }, + sourcePath: { + type: 'string', + description: + 'Relative to the manifest URL/base URL, or an absolute URL.', + }, + extraFiles: { + $ref: '#/definitions/PHPExtensionManifestExtraFiles', + description: + 'URL-backed files needed only by this artifact.', + }, + }, + required: ['phpVersion', 'sourcePath'], + additionalProperties: false, + }, }, extraFiles: { $ref: '#/definitions/PHPExtensionManifestExtraFiles', @@ -113,24 +178,10 @@ const schema12 = { 'Extension artifact manifest. Lets callers publish a matrix of `.so` files and lets `resolvePHPExtension()` select the artifact matching the current PHP version. External extension artifacts are JSPI-only.', }; const schema13 = { - type: 'object', - properties: { - phpVersion: { - type: 'string', - description: 'PHP major/minor version, e.g. `8.4`.', - }, - sourcePath: { - type: 'string', - description: - 'Relative to the manifest URL/base URL, or an absolute URL.', - }, - extraFiles: { - $ref: '#/definitions/PHPExtensionManifestExtraFiles', - description: 'URL-backed files needed only by this artifact.', - }, - }, - required: ['phpVersion', 'sourcePath'], - additionalProperties: false, + type: 'string', + enum: ['extension', 'zend_extension'], + description: + 'The php.ini directive used to load the extension. Use `extension` for regular PHP extensions and `zend_extension` for Zend extensions like Xdebug.', }; const schema14 = { type: 'object', @@ -142,491 +193,34 @@ const schema14 = { }, nodes: { type: 'array', - items: { $ref: '#/definitions/PHPExtensionManifestExtraFile' }, - }, - }, - additionalProperties: false, -}; -const schema15 = { - type: 'object', - properties: { - vfsPath: { - type: 'string', - description: - "Joined with the group's `vfsRoot` to form the final VFS path.", - }, - type: { - type: 'string', - enum: ['file', 'directory'], - description: - 'Defaults to "file". Only file nodes need a `sourcePath`.', - }, - sourcePath: { - type: 'string', - description: - 'Relative to the manifest URL/base URL, or an absolute URL.', + items: { + type: 'object', + properties: { + vfsPath: { + type: 'string', + description: + "Joined with the group's `vfsRoot` to form the final VFS path.", + }, + type: { + type: 'string', + enum: ['file', 'directory'], + description: + 'Defaults to "file". Only file nodes need a `sourcePath`.', + }, + sourcePath: { + type: 'string', + description: + 'Relative to the manifest URL/base URL, or an absolute URL.', + }, + }, + required: ['vfsPath'], + additionalProperties: false, + }, }, }, - required: ['vfsPath'], additionalProperties: false, }; -function validate13( - data, - { instancePath = '', parentData, parentDataProperty, rootData = data } = {} -) { - let vErrors = null; - let errors = 0; - if (errors === 0) { - if (data && typeof data == 'object' && !Array.isArray(data)) { - const _errs1 = errors; - for (const key0 in data) { - if (!(key0 === 'vfsRoot' || key0 === 'nodes')) { - validate13.errors = [ - { - instancePath, - schemaPath: '#/additionalProperties', - keyword: 'additionalProperties', - params: { additionalProperty: key0 }, - message: 'must NOT have additional properties', - }, - ]; - return false; - break; - } - } - if (_errs1 === errors) { - if (data.vfsRoot !== undefined) { - const _errs2 = errors; - if (typeof data.vfsRoot !== 'string') { - validate13.errors = [ - { - instancePath: instancePath + '/vfsRoot', - schemaPath: '#/properties/vfsRoot/type', - keyword: 'type', - params: { type: 'string' }, - message: 'must be string', - }, - ]; - return false; - } - var valid0 = _errs2 === errors; - } else { - var valid0 = true; - } - if (valid0) { - if (data.nodes !== undefined) { - let data1 = data.nodes; - const _errs4 = errors; - if (errors === _errs4) { - if (Array.isArray(data1)) { - var valid1 = true; - const len0 = data1.length; - for (let i0 = 0; i0 < len0; i0++) { - let data2 = data1[i0]; - const _errs6 = errors; - const _errs7 = errors; - if (errors === _errs7) { - if ( - data2 && - typeof data2 == 'object' && - !Array.isArray(data2) - ) { - let missing0; - if ( - data2.vfsPath === undefined && - (missing0 = 'vfsPath') - ) { - validate13.errors = [ - { - instancePath: - instancePath + - '/nodes/' + - i0, - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/required', - keyword: 'required', - params: { - missingProperty: - missing0, - }, - message: - "must have required property '" + - missing0 + - "'", - }, - ]; - return false; - } else { - const _errs9 = errors; - for (const key1 in data2) { - if ( - !( - key1 === - 'vfsPath' || - key1 === 'type' || - key1 === - 'sourcePath' - ) - ) { - validate13.errors = [ - { - instancePath: - instancePath + - '/nodes/' + - i0, - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/additionalProperties', - keyword: - 'additionalProperties', - params: { - additionalProperty: - key1, - }, - message: - 'must NOT have additional properties', - }, - ]; - return false; - break; - } - } - if (_errs9 === errors) { - if ( - data2.vfsPath !== - undefined - ) { - const _errs10 = errors; - if ( - typeof data2.vfsPath !== - 'string' - ) { - validate13.errors = - [ - { - instancePath: - instancePath + - '/nodes/' + - i0 + - '/vfsPath', - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/properties/vfsPath/type', - keyword: - 'type', - params: { - type: 'string', - }, - message: - 'must be string', - }, - ]; - return false; - } - var valid3 = - _errs10 === errors; - } else { - var valid3 = true; - } - if (valid3) { - if ( - data2.type !== - undefined - ) { - let data4 = - data2.type; - const _errs12 = - errors; - if ( - typeof data4 !== - 'string' - ) { - validate13.errors = - [ - { - instancePath: - instancePath + - '/nodes/' + - i0 + - '/type', - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/properties/type/type', - keyword: - 'type', - params: { - type: 'string', - }, - message: - 'must be string', - }, - ]; - return false; - } - if ( - !( - data4 === - 'file' || - data4 === - 'directory' - ) - ) { - validate13.errors = - [ - { - instancePath: - instancePath + - '/nodes/' + - i0 + - '/type', - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/properties/type/enum', - keyword: - 'enum', - params: { - allowedValues: - schema15 - .properties - .type - .enum, - }, - message: - 'must be equal to one of the allowed values', - }, - ]; - return false; - } - var valid3 = - _errs12 === - errors; - } else { - var valid3 = true; - } - if (valid3) { - if ( - data2.sourcePath !== - undefined - ) { - const _errs14 = - errors; - if ( - typeof data2.sourcePath !== - 'string' - ) { - validate13.errors = - [ - { - instancePath: - instancePath + - '/nodes/' + - i0 + - '/sourcePath', - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/properties/sourcePath/type', - keyword: - 'type', - params: { - type: 'string', - }, - message: - 'must be string', - }, - ]; - return false; - } - var valid3 = - _errs14 === - errors; - } else { - var valid3 = true; - } - } - } - } - } - } else { - validate13.errors = [ - { - instancePath: - instancePath + - '/nodes/' + - i0, - schemaPath: - '#/definitions/PHPExtensionManifestExtraFile/type', - keyword: 'type', - params: { type: 'object' }, - message: 'must be object', - }, - ]; - return false; - } - } - var valid1 = _errs6 === errors; - if (!valid1) { - break; - } - } - } else { - validate13.errors = [ - { - instancePath: instancePath + '/nodes', - schemaPath: '#/properties/nodes/type', - keyword: 'type', - params: { type: 'array' }, - message: 'must be array', - }, - ]; - return false; - } - } - var valid0 = _errs4 === errors; - } else { - var valid0 = true; - } - } - } - } else { - validate13.errors = [ - { - instancePath, - schemaPath: '#/type', - keyword: 'type', - params: { type: 'object' }, - message: 'must be object', - }, - ]; - return false; - } - } - validate13.errors = vErrors; - return errors === 0; -} -function validate12( - data, - { instancePath = '', parentData, parentDataProperty, rootData = data } = {} -) { - let vErrors = null; - let errors = 0; - if (errors === 0) { - if (data && typeof data == 'object' && !Array.isArray(data)) { - let missing0; - if ( - (data.phpVersion === undefined && (missing0 = 'phpVersion')) || - (data.sourcePath === undefined && (missing0 = 'sourcePath')) - ) { - validate12.errors = [ - { - instancePath, - schemaPath: '#/required', - keyword: 'required', - params: { missingProperty: missing0 }, - message: - "must have required property '" + missing0 + "'", - }, - ]; - return false; - } else { - const _errs1 = errors; - for (const key0 in data) { - if ( - !( - key0 === 'phpVersion' || - key0 === 'sourcePath' || - key0 === 'extraFiles' - ) - ) { - validate12.errors = [ - { - instancePath, - schemaPath: '#/additionalProperties', - keyword: 'additionalProperties', - params: { additionalProperty: key0 }, - message: 'must NOT have additional properties', - }, - ]; - return false; - break; - } - } - if (_errs1 === errors) { - if (data.phpVersion !== undefined) { - const _errs2 = errors; - if (typeof data.phpVersion !== 'string') { - validate12.errors = [ - { - instancePath: instancePath + '/phpVersion', - schemaPath: '#/properties/phpVersion/type', - keyword: 'type', - params: { type: 'string' }, - message: 'must be string', - }, - ]; - return false; - } - var valid0 = _errs2 === errors; - } else { - var valid0 = true; - } - if (valid0) { - if (data.sourcePath !== undefined) { - const _errs4 = errors; - if (typeof data.sourcePath !== 'string') { - validate12.errors = [ - { - instancePath: - instancePath + '/sourcePath', - schemaPath: - '#/properties/sourcePath/type', - keyword: 'type', - params: { type: 'string' }, - message: 'must be string', - }, - ]; - return false; - } - var valid0 = _errs4 === errors; - } else { - var valid0 = true; - } - if (valid0) { - if (data.extraFiles !== undefined) { - const _errs6 = errors; - if ( - !validate13(data.extraFiles, { - instancePath: - instancePath + '/extraFiles', - parentData: data, - parentDataProperty: 'extraFiles', - rootData, - }) - ) { - vErrors = - vErrors === null - ? validate13.errors - : vErrors.concat(validate13.errors); - errors = vErrors.length; - } - var valid0 = _errs6 === errors; - } else { - var valid0 = true; - } - } - } - } - } - } else { - validate12.errors = [ - { - instancePath, - schemaPath: '#/type', - keyword: 'type', - params: { type: 'object' }, - message: 'must be object', - }, - ]; - return false; - } - } - validate12.errors = vErrors; - return errors === 0; -} +const func2 = Object.prototype.hasOwnProperty; function validate11( data, { instancePath = '', parentData, parentDataProperty, rootData = data } = {} @@ -654,15 +248,7 @@ function validate11( } else { const _errs1 = errors; for (const key0 in data) { - if ( - !( - key0 === 'name' || - key0 === 'version' || - key0 === 'mode' || - key0 === 'artifacts' || - key0 === 'extraFiles' - ) - ) { + if (!func2.call(schema12.properties, key0)) { validate11.errors = [ { instancePath, @@ -754,85 +340,1411 @@ function validate11( var valid0 = true; } if (valid0) { - if (data.artifacts !== undefined) { - let data3 = data.artifacts; + if (data.loadWithIniDirective !== undefined) { + let data3 = data.loadWithIniDirective; const _errs8 = errors; - if (errors === _errs8) { - if (Array.isArray(data3)) { - var valid1 = true; - const len0 = data3.length; - for (let i0 = 0; i0 < len0; i0++) { - const _errs10 = errors; - if ( - !validate12(data3[i0], { - instancePath: - instancePath + - '/artifacts/' + - i0, - parentData: data3, - parentDataProperty: i0, - rootData, - }) - ) { - vErrors = - vErrors === null - ? validate12.errors - : vErrors.concat( - validate12.errors - ); - errors = vErrors.length; - } - var valid1 = _errs10 === errors; - if (!valid1) { - break; - } - } - } else { - validate11.errors = [ - { - instancePath: - instancePath + - '/artifacts', - schemaPath: - '#/properties/artifacts/type', - keyword: 'type', - params: { type: 'array' }, - message: 'must be array', + if (typeof data3 !== 'string') { + validate11.errors = [ + { + instancePath: + instancePath + + '/loadWithIniDirective', + schemaPath: + '#/definitions/PHPExtensionIniDirective/type', + keyword: 'type', + params: { type: 'string' }, + message: 'must be string', + }, + ]; + return false; + } + if ( + !( + data3 === 'extension' || + data3 === 'zend_extension' + ) + ) { + validate11.errors = [ + { + instancePath: + instancePath + + '/loadWithIniDirective', + schemaPath: + '#/definitions/PHPExtensionIniDirective/enum', + keyword: 'enum', + params: { + allowedValues: + schema13.enum, }, - ]; - return false; - } + message: + 'must be equal to one of the allowed values', + }, + ]; + return false; } var valid0 = _errs8 === errors; } else { var valid0 = true; } if (valid0) { - if (data.extraFiles !== undefined) { + if (data.iniEntries !== undefined) { + let data4 = data.iniEntries; const _errs11 = errors; - if ( - !validate13(data.extraFiles, { - instancePath: - instancePath + - '/extraFiles', - parentData: data, - parentDataProperty: - 'extraFiles', - rootData, - }) - ) { - vErrors = - vErrors === null - ? validate13.errors - : vErrors.concat( - validate13.errors - ); - errors = vErrors.length; + if (errors === _errs11) { + if ( + data4 && + typeof data4 == 'object' && + !Array.isArray(data4) + ) { + for (const key1 in data4) { + const _errs14 = errors; + if ( + typeof data4[key1] !== + 'string' + ) { + validate11.errors = [ + { + instancePath: + instancePath + + '/iniEntries/' + + key1 + .replace( + /~/g, + '~0' + ) + .replace( + /\//g, + '~1' + ), + schemaPath: + '#/properties/iniEntries/additionalProperties/type', + keyword: 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid2 = + _errs14 === errors; + if (!valid2) { + break; + } + } + } else { + validate11.errors = [ + { + instancePath: + instancePath + + '/iniEntries', + schemaPath: + '#/properties/iniEntries/type', + keyword: 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } } var valid0 = _errs11 === errors; } else { var valid0 = true; } + if (valid0) { + if (data.env !== undefined) { + let data6 = data.env; + const _errs16 = errors; + if (errors === _errs16) { + if ( + data6 && + typeof data6 == 'object' && + !Array.isArray(data6) + ) { + for (const key2 in data6) { + const _errs19 = errors; + if ( + typeof data6[ + key2 + ] !== 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/env/' + + key2 + .replace( + /~/g, + '~0' + ) + .replace( + /\//g, + '~1' + ), + schemaPath: + '#/properties/env/additionalProperties/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid3 = + _errs19 === errors; + if (!valid3) { + break; + } + } + } else { + validate11.errors = [ + { + instancePath: + instancePath + + '/env', + schemaPath: + '#/properties/env/type', + keyword: 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid0 = _errs16 === errors; + } else { + var valid0 = true; + } + if (valid0) { + if ( + data.extensionDir !== undefined + ) { + const _errs21 = errors; + if ( + typeof data.extensionDir !== + 'string' + ) { + validate11.errors = [ + { + instancePath: + instancePath + + '/extensionDir', + schemaPath: + '#/properties/extensionDir/type', + keyword: 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid0 = _errs21 === errors; + } else { + var valid0 = true; + } + if (valid0) { + if ( + data.artifacts !== undefined + ) { + let data9 = data.artifacts; + const _errs23 = errors; + if (errors === _errs23) { + if ( + Array.isArray(data9) + ) { + var valid4 = true; + const len0 = + data9.length; + for ( + let i0 = 0; + i0 < len0; + i0++ + ) { + let data10 = + data9[i0]; + const _errs25 = + errors; + if ( + errors === + _errs25 + ) { + if ( + data10 && + typeof data10 == + 'object' && + !Array.isArray( + data10 + ) + ) { + let missing1; + if ( + (data10.phpVersion === + undefined && + (missing1 = + 'phpVersion')) || + (data10.sourcePath === + undefined && + (missing1 = + 'sourcePath')) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0, + schemaPath: + '#/properties/artifacts/items/required', + keyword: + 'required', + params: { + missingProperty: + missing1, + }, + message: + "must have required property '" + + missing1 + + "'", + }, + ]; + return false; + } else { + const _errs27 = + errors; + for (const key3 in data10) { + if ( + !( + key3 === + 'phpVersion' || + key3 === + 'sourcePath' || + key3 === + 'extraFiles' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0, + schemaPath: + '#/properties/artifacts/items/additionalProperties', + keyword: + 'additionalProperties', + params: { + additionalProperty: + key3, + }, + message: + 'must NOT have additional properties', + }, + ]; + return false; + break; + } + } + if ( + _errs27 === + errors + ) { + if ( + data10.phpVersion !== + undefined + ) { + const _errs28 = + errors; + if ( + typeof data10.phpVersion !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/phpVersion', + schemaPath: + '#/properties/artifacts/items/properties/phpVersion/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid5 = + _errs28 === + errors; + } else { + var valid5 = true; + } + if ( + valid5 + ) { + if ( + data10.sourcePath !== + undefined + ) { + const _errs30 = + errors; + if ( + typeof data10.sourcePath !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/sourcePath', + schemaPath: + '#/properties/artifacts/items/properties/sourcePath/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid5 = + _errs30 === + errors; + } else { + var valid5 = true; + } + if ( + valid5 + ) { + if ( + data10.extraFiles !== + undefined + ) { + let data13 = + data10.extraFiles; + const _errs32 = + errors; + const _errs33 = + errors; + if ( + errors === + _errs33 + ) { + if ( + data13 && + typeof data13 == + 'object' && + !Array.isArray( + data13 + ) + ) { + const _errs35 = + errors; + for (const key4 in data13) { + if ( + !( + key4 === + 'vfsRoot' || + key4 === + 'nodes' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/additionalProperties', + keyword: + 'additionalProperties', + params: { + additionalProperty: + key4, + }, + message: + 'must NOT have additional properties', + }, + ]; + return false; + break; + } + } + if ( + _errs35 === + errors + ) { + if ( + data13.vfsRoot !== + undefined + ) { + const _errs36 = + errors; + if ( + typeof data13.vfsRoot !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/vfsRoot', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/vfsRoot/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid7 = + _errs36 === + errors; + } else { + var valid7 = true; + } + if ( + valid7 + ) { + if ( + data13.nodes !== + undefined + ) { + let data15 = + data13.nodes; + const _errs38 = + errors; + if ( + errors === + _errs38 + ) { + if ( + Array.isArray( + data15 + ) + ) { + var valid8 = true; + const len1 = + data15.length; + for ( + let i1 = 0; + i1 < + len1; + i1++ + ) { + let data16 = + data15[ + i1 + ]; + const _errs40 = + errors; + if ( + errors === + _errs40 + ) { + if ( + data16 && + typeof data16 == + 'object' && + !Array.isArray( + data16 + ) + ) { + let missing2; + if ( + data16.vfsPath === + undefined && + (missing2 = + 'vfsPath') + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/required', + keyword: + 'required', + params: { + missingProperty: + missing2, + }, + message: + "must have required property '" + + missing2 + + "'", + }, + ]; + return false; + } else { + const _errs42 = + errors; + for (const key5 in data16) { + if ( + !( + key5 === + 'vfsPath' || + key5 === + 'type' || + key5 === + 'sourcePath' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/additionalProperties', + keyword: + 'additionalProperties', + params: { + additionalProperty: + key5, + }, + message: + 'must NOT have additional properties', + }, + ]; + return false; + break; + } + } + if ( + _errs42 === + errors + ) { + if ( + data16.vfsPath !== + undefined + ) { + const _errs43 = + errors; + if ( + typeof data16.vfsPath !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1 + + '/vfsPath', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/vfsPath/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid9 = + _errs43 === + errors; + } else { + var valid9 = true; + } + if ( + valid9 + ) { + if ( + data16.type !== + undefined + ) { + let data18 = + data16.type; + const _errs45 = + errors; + if ( + typeof data18 !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1 + + '/type', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/type/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + if ( + !( + data18 === + 'file' || + data18 === + 'directory' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1 + + '/type', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/type/enum', + keyword: + 'enum', + params: { + allowedValues: + schema14 + .properties + .nodes + .items + .properties + .type + .enum, + }, + message: + 'must be equal to one of the allowed values', + }, + ]; + return false; + } + var valid9 = + _errs45 === + errors; + } else { + var valid9 = true; + } + if ( + valid9 + ) { + if ( + data16.sourcePath !== + undefined + ) { + const _errs47 = + errors; + if ( + typeof data16.sourcePath !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1 + + '/sourcePath', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/sourcePath/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid9 = + _errs47 === + errors; + } else { + var valid9 = true; + } + } + } + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes/' + + i1, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/type', + keyword: + 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid8 = + _errs40 === + errors; + if ( + !valid8 + ) { + break; + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles/nodes', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/type', + keyword: + 'type', + params: { + type: 'array', + }, + message: + 'must be array', + }, + ]; + return false; + } + } + var valid7 = + _errs38 === + errors; + } else { + var valid7 = true; + } + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0 + + '/extraFiles', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/type', + keyword: + 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid5 = + _errs32 === + errors; + } else { + var valid5 = true; + } + } + } + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts/' + + i0, + schemaPath: + '#/properties/artifacts/items/type', + keyword: + 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid4 = + _errs25 === + errors; + if (!valid4) { + break; + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/artifacts', + schemaPath: + '#/properties/artifacts/type', + keyword: + 'type', + params: { + type: 'array', + }, + message: + 'must be array', + }, + ]; + return false; + } + } + var valid0 = + _errs23 === errors; + } else { + var valid0 = true; + } + if (valid0) { + if ( + data.extraFiles !== + undefined + ) { + let data20 = + data.extraFiles; + const _errs49 = errors; + const _errs50 = errors; + if ( + errors === _errs50 + ) { + if ( + data20 && + typeof data20 == + 'object' && + !Array.isArray( + data20 + ) + ) { + const _errs52 = + errors; + for (const key6 in data20) { + if ( + !( + key6 === + 'vfsRoot' || + key6 === + 'nodes' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/additionalProperties', + keyword: + 'additionalProperties', + params: { + additionalProperty: + key6, + }, + message: + 'must NOT have additional properties', + }, + ]; + return false; + break; + } + } + if ( + _errs52 === + errors + ) { + if ( + data20.vfsRoot !== + undefined + ) { + const _errs53 = + errors; + if ( + typeof data20.vfsRoot !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/vfsRoot', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/vfsRoot/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid11 = + _errs53 === + errors; + } else { + var valid11 = true; + } + if ( + valid11 + ) { + if ( + data20.nodes !== + undefined + ) { + let data22 = + data20.nodes; + const _errs55 = + errors; + if ( + errors === + _errs55 + ) { + if ( + Array.isArray( + data22 + ) + ) { + var valid12 = true; + const len2 = + data22.length; + for ( + let i2 = 0; + i2 < + len2; + i2++ + ) { + let data23 = + data22[ + i2 + ]; + const _errs57 = + errors; + if ( + errors === + _errs57 + ) { + if ( + data23 && + typeof data23 == + 'object' && + !Array.isArray( + data23 + ) + ) { + let missing3; + if ( + data23.vfsPath === + undefined && + (missing3 = + 'vfsPath') + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/required', + keyword: + 'required', + params: { + missingProperty: + missing3, + }, + message: + "must have required property '" + + missing3 + + "'", + }, + ]; + return false; + } else { + const _errs59 = + errors; + for (const key7 in data23) { + if ( + !( + key7 === + 'vfsPath' || + key7 === + 'type' || + key7 === + 'sourcePath' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/additionalProperties', + keyword: + 'additionalProperties', + params: { + additionalProperty: + key7, + }, + message: + 'must NOT have additional properties', + }, + ]; + return false; + break; + } + } + if ( + _errs59 === + errors + ) { + if ( + data23.vfsPath !== + undefined + ) { + const _errs60 = + errors; + if ( + typeof data23.vfsPath !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2 + + '/vfsPath', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/vfsPath/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid13 = + _errs60 === + errors; + } else { + var valid13 = true; + } + if ( + valid13 + ) { + if ( + data23.type !== + undefined + ) { + let data25 = + data23.type; + const _errs62 = + errors; + if ( + typeof data25 !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2 + + '/type', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/type/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + if ( + !( + data25 === + 'file' || + data25 === + 'directory' + ) + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2 + + '/type', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/type/enum', + keyword: + 'enum', + params: { + allowedValues: + schema14 + .properties + .nodes + .items + .properties + .type + .enum, + }, + message: + 'must be equal to one of the allowed values', + }, + ]; + return false; + } + var valid13 = + _errs62 === + errors; + } else { + var valid13 = true; + } + if ( + valid13 + ) { + if ( + data23.sourcePath !== + undefined + ) { + const _errs64 = + errors; + if ( + typeof data23.sourcePath !== + 'string' + ) { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2 + + '/sourcePath', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/properties/sourcePath/type', + keyword: + 'type', + params: { + type: 'string', + }, + message: + 'must be string', + }, + ]; + return false; + } + var valid13 = + _errs64 === + errors; + } else { + var valid13 = true; + } + } + } + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes/' + + i2, + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/items/type', + keyword: + 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid12 = + _errs57 === + errors; + if ( + !valid12 + ) { + break; + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles/nodes', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/properties/nodes/type', + keyword: + 'type', + params: { + type: 'array', + }, + message: + 'must be array', + }, + ]; + return false; + } + } + var valid11 = + _errs55 === + errors; + } else { + var valid11 = true; + } + } + } + } else { + validate11.errors = + [ + { + instancePath: + instancePath + + '/extraFiles', + schemaPath: + '#/definitions/PHPExtensionManifestExtraFiles/type', + keyword: + 'type', + params: { + type: 'object', + }, + message: + 'must be object', + }, + ]; + return false; + } + } + var valid0 = + _errs49 === errors; + } else { + var valid0 = true; + } + } + } + } + } } } } diff --git a/packages/php-wasm/universal/public/php-extension-manifest-schema.json b/packages/php-wasm/universal/public/php-extension-manifest-schema.json index d9fc6655934..88962edffa5 100644 --- a/packages/php-wasm/universal/public/php-extension-manifest-schema.json +++ b/packages/php-wasm/universal/public/php-extension-manifest-schema.json @@ -15,10 +15,48 @@ "type": "string", "const": "php-extension" }, + "loadWithIniDirective": { + "$ref": "#/definitions/PHPExtensionIniDirective", + "description": "The first directive of the generated startup `.ini` file. Defaults to `extension`; use `zend_extension` for Zend extensions like Xdebug." + }, + "iniEntries": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Additional `key=value` lines for the generated startup `.ini` file." + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables added before the extension is loaded." + }, + "extensionDir": { + "type": "string", + "description": "VFS directory where PHP.wasm writes the extension `.so` file and its per-extension ini file. Defaults to `PHP_EXTENSIONS_DIR`." + }, "artifacts": { "type": "array", "items": { - "$ref": "#/definitions/PHPExtensionManifestArtifact" + "type": "object", + "properties": { + "phpVersion": { + "type": "string", + "description": "PHP major/minor version, e.g. `8.4`." + }, + "sourcePath": { + "type": "string", + "description": "Relative to the manifest URL/base URL, or an absolute URL." + }, + "extraFiles": { + "$ref": "#/definitions/PHPExtensionManifestExtraFiles", + "description": "URL-backed files needed only by this artifact." + } + }, + "required": ["phpVersion", "sourcePath"], + "additionalProperties": false } }, "extraFiles": { @@ -30,24 +68,10 @@ "additionalProperties": false, "description": "Extension artifact manifest. Lets callers publish a matrix of `.so` files and lets `resolvePHPExtension()` select the artifact matching the current PHP version. External extension artifacts are JSPI-only." }, - "PHPExtensionManifestArtifact": { - "type": "object", - "properties": { - "phpVersion": { - "type": "string", - "description": "PHP major/minor version, e.g. `8.4`." - }, - "sourcePath": { - "type": "string", - "description": "Relative to the manifest URL/base URL, or an absolute URL." - }, - "extraFiles": { - "$ref": "#/definitions/PHPExtensionManifestExtraFiles", - "description": "URL-backed files needed only by this artifact." - } - }, - "required": ["phpVersion", "sourcePath"], - "additionalProperties": false + "PHPExtensionIniDirective": { + "type": "string", + "enum": ["extension", "zend_extension"], + "description": "The php.ini directive used to load the extension. Use `extension` for regular PHP extensions and `zend_extension` for Zend extensions like Xdebug." }, "PHPExtensionManifestExtraFiles": { "type": "object", @@ -59,31 +83,28 @@ "nodes": { "type": "array", "items": { - "$ref": "#/definitions/PHPExtensionManifestExtraFile" + "type": "object", + "properties": { + "vfsPath": { + "type": "string", + "description": "Joined with the group's `vfsRoot` to form the final VFS path." + }, + "type": { + "type": "string", + "enum": ["file", "directory"], + "description": "Defaults to \"file\". Only file nodes need a `sourcePath`." + }, + "sourcePath": { + "type": "string", + "description": "Relative to the manifest URL/base URL, or an absolute URL." + } + }, + "required": ["vfsPath"], + "additionalProperties": false } } }, "additionalProperties": false - }, - "PHPExtensionManifestExtraFile": { - "type": "object", - "properties": { - "vfsPath": { - "type": "string", - "description": "Joined with the group's `vfsRoot` to form the final VFS path." - }, - "type": { - "type": "string", - "enum": ["file", "directory"], - "description": "Defaults to \"file\". Only file nodes need a `sourcePath`." - }, - "sourcePath": { - "type": "string", - "description": "Relative to the manifest URL/base URL, or an absolute URL." - } - }, - "required": ["vfsPath"], - "additionalProperties": false } } } diff --git a/packages/php-wasm/universal/src/lib/load-extension.spec.ts b/packages/php-wasm/universal/src/lib/load-extension.spec.ts index 959d770535f..af49b7bae27 100644 --- a/packages/php-wasm/universal/src/lib/load-extension.spec.ts +++ b/packages/php-wasm/universal/src/lib/load-extension.spec.ts @@ -80,6 +80,42 @@ describe('resolvePHPExtension', () => { expect(extension.soBytes).toEqual(artifactBytes); }); + it('applies manifest-declared runtime settings', async () => { + const extension = await resolvePHPExtension({ + source: { + format: 'manifest', + manifest: { + name: 'spx', + artifacts: [ + { + phpVersion: '8.4', + sourcePath: 'spx.so', + }, + ], + extensionDir: '/internal/shared/extensions', + iniEntries: { + 'spx.http_enabled': '1', + }, + env: { + SPX_DATA_DIR: '/internal/shared/spx/data', + }, + }, + baseUrl: 'https://example.com/extensions/', + }, + phpVersion: '8.4', + fetch: async () => new Response(new Uint8Array([1, 2, 3])), + }); + + expect(extension.soPath).toBe('/internal/shared/extensions/spx.so'); + expect(extension.iniContent).toBe( + 'extension=/internal/shared/extensions/spx.so\n' + + 'spx.http_enabled=1' + ); + expect(extension.env).toEqual({ + SPX_DATA_DIR: '/internal/shared/spx/data', + }); + }); + it('rejects manifests that do not match the generated schema validator', async () => { await expect( resolvePHPExtension({ diff --git a/packages/php-wasm/universal/src/lib/load-extension.ts b/packages/php-wasm/universal/src/lib/load-extension.ts index 91359c4f083..6c87ada9cd1 100644 --- a/packages/php-wasm/universal/src/lib/load-extension.ts +++ b/packages/php-wasm/universal/src/lib/load-extension.ts @@ -113,6 +113,20 @@ export interface PHPExtensionManifest { name: string; version?: string; mode?: 'php-extension'; + /** + * The first directive of the generated startup `.ini` file. Defaults to + * `extension`; use `zend_extension` for Zend extensions like Xdebug. + */ + loadWithIniDirective?: PHPExtensionIniDirective; + /** Additional `key=value` lines for the generated startup `.ini` file. */ + iniEntries?: Record; + /** Environment variables added before the extension is loaded. */ + env?: Record; + /** + * VFS directory where PHP.wasm writes the extension `.so` file and its + * per-extension ini file. Defaults to `PHP_EXTENSIONS_DIR`. + */ + extensionDir?: string; artifacts: Array<{ /** PHP major/minor version, e.g. `8.4`. */ phpVersion: string; @@ -300,6 +314,10 @@ export async function resolvePHPExtension( let soBytes: Uint8Array; const files: Record = {}; const directories: string[] = []; + let manifestLoadWithIniDirective: PHPExtensionIniDirective | undefined; + let manifestIniEntries: Record | undefined; + let manifestEnv: Record | undefined; + let manifestExtensionDir: string | undefined; if (source.format === 'so') { if (!name) { @@ -369,6 +387,10 @@ export async function resolvePHPExtension( ); } name ??= manifest.name; + manifestLoadWithIniDirective = manifest.loadWithIniDirective; + manifestIniEntries = manifest.iniEntries; + manifestEnv = manifest.env; + manifestExtensionDir = manifest.extensionDir; const queue = new Semaphore({ concurrency: MAX_EXTENSION_SIDECAR_FILE_REQUESTS, @@ -400,22 +422,31 @@ export async function resolvePHPExtension( } const extensionDir = normalizePath( - options.extensionDir ?? PHP_EXTENSIONS_DIR + options.extensionDir ?? manifestExtensionDir ?? PHP_EXTENSIONS_DIR ); if (options.extraFiles) { Object.assign(files, options.extraFiles.files); directories.push(...(options.extraFiles.directories ?? [])); } - const directive = options.loadWithIniDirective ?? 'extension'; + const directive = + options.loadWithIniDirective ?? + manifestLoadWithIniDirective ?? + 'extension'; + const iniEntries = { + ...manifestIniEntries, + ...options.iniEntries, + }; const soPath = joinPaths(extensionDir, `${name}.so`); const iniPath = joinPaths(extensionDir, `${name}.ini`); const iniContent = [ `${directive}=${soPath}`, - ...Object.entries(options.iniEntries ?? {}).map( - ([key, value]) => `${key}=${value}` - ), + ...Object.entries(iniEntries).map(([key, value]) => `${key}=${value}`), ].join('\n'); + const env = { + ...manifestEnv, + ...options.env, + }; return { soPath, @@ -426,7 +457,7 @@ export async function resolvePHPExtension( files, directories, }, - env: options.env, + env: Object.keys(env).length ? env : undefined, extensionDir, }; } diff --git a/packages/playground/cli/README.md b/packages/playground/cli/README.md index bf9c085626c..9d64391ee44 100644 --- a/packages/playground/cli/README.md +++ b/packages/playground/cli/README.md @@ -101,7 +101,6 @@ The `server` command supports the following optional arguments: - `--phpmyadmin[=]`: Install phpMyAdmin for database management. The phpMyAdmin URL will be printed after boot. Optionally specify a custom URL path (default: `/phpmyadmin`). - `--internal-cookie-store`: Enables Playground's internal cookie handling. When active, Playground uses an HttpCookieStore to manage and persist cookies across requests. If disabled, cookies are handled externally, like by a browser in Node.js. - `--php-extension=`: Load a custom PHP.wasm extension manifest before PHP starts. Accepts local paths, `file:` URLs, and `http(s):` URLs. Can be used multiple times. -- `--php-extension-config=`: Load a JSON extension config before PHP starts. Use this for direct `.so` URLs or extension-specific `iniEntries` and `env` settings. Can be used multiple times. ### Loading Custom PHP.wasm Extensions @@ -118,14 +117,18 @@ The manifest selects the `.so` artifact matching the active PHP version and can stage sidecar files before PHP starts. External extensions are JSPI-only, so use Node.js 23 or newer. -Use `--php-extension-config` when the extension needs more runtime settings: +Add runtime settings such as `iniEntries` and `env` directly to the manifest: ```json { - "source": { - "format": "manifest", - "manifestUrl": "./dist/spx/manifest.json" - }, + "name": "spx", + "version": "0.1.0", + "artifacts": [ + { + "phpVersion": "8.4", + "sourcePath": "spx-php8.4-jspi.so" + } + ], "iniEntries": { "spx.http_enabled": "1" }, @@ -135,10 +138,6 @@ Use `--php-extension-config` when the extension needs more runtime settings: } ``` -```bash -npx @wp-playground/cli@latest server --php-extension-config=./spx.json -``` - ## Need some help with the CLI? With the Playground CLI, you can use the `--help` to get some support about the available commands. diff --git a/packages/playground/cli/src/php-extensions.ts b/packages/playground/cli/src/php-extensions.ts index f0fb9c60bc4..a2727e6591b 100644 --- a/packages/playground/cli/src/php-extensions.ts +++ b/packages/playground/cli/src/php-extensions.ts @@ -1,16 +1,11 @@ -import { readFileSync } from 'node:fs'; -import type { - PHPExtension, - RuntimePHPExtensionSource, - XdebugOptions, -} from '@php-wasm/node'; +import type { PHPExtension, XdebugOptions } from '@php-wasm/node'; /** * Converts Playground CLI extension options into the runtime `extensions` * array. * * The CLI receives built-in extensions as individual options (`intl`, `redis`, - * `memcached`, and `xdebug`) and external extensions as manifest/config paths. + * `memcached`, and `xdebug`) and external extensions as manifest paths. * The PHP runtime expects one array that can contain built-in names and * external extension sources side by side. * @@ -24,7 +19,6 @@ export function cliExtensionArgsToExtensionsArray(args: { memcached?: boolean; xdebug?: boolean | XdebugOptions; phpExtension?: string[]; - phpExtensionConfig?: string[]; }): PHPExtension[] { const extensions: PHPExtension[] = []; if (args.intl) { @@ -51,61 +45,5 @@ export function cliExtensionArgsToExtensionsArray(args: { }, }); } - for (const configPath of args.phpExtensionConfig || []) { - extensions.push(readPHPExtensionConfig(configPath)); - } return extensions; } - -export function readPHPExtensionConfig( - configPath: string -): RuntimePHPExtensionSource { - let config: unknown; - try { - config = JSON.parse(readFileSync(configPath, 'utf8')); - } catch (error) { - throw new Error(`Could not read PHP extension config: ${configPath}`, { - cause: error, - }); - } - - if (!isRecord(config) || !isRecord(config['source'])) { - throw new Error( - `Invalid PHP extension config: ${configPath}. Expected an object with a source field.` - ); - } - - const source = config['source']; - if (source['format'] === 'so') { - throw new Error( - `Invalid PHP extension config: ${configPath}. The CLI cannot load direct bytes; use a manifest or URL source.` - ); - } - if (source['format'] === 'url') { - if (typeof source['url'] !== 'string') { - throw new Error( - `Invalid PHP extension config: ${configPath}. A URL source requires a string url.` - ); - } - return config as RuntimePHPExtensionSource; - } - if (source['format'] === 'manifest') { - if ( - typeof source['manifestUrl'] !== 'string' && - !isRecord(source['manifest']) - ) { - throw new Error( - `Invalid PHP extension config: ${configPath}. A manifest source requires manifestUrl or manifest.` - ); - } - return config as RuntimePHPExtensionSource; - } - - throw new Error( - `Invalid PHP extension config: ${configPath}. Unknown source format.` - ); -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 31de2ad2134..33476d5de75 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -306,13 +306,6 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { string: true, nargs: 1, }, - 'php-extension-config': { - describe: - 'Load a JSON PHP.wasm extension config before PHP starts. Use this for direct .so URLs or extension-specific ini/env settings. Can be used multiple times.', - type: 'array', - string: true, - nargs: 1, - }, 'experimental-unsafe-ide-integration': { describe: 'Enable experimental IDE development tools. This option edits IDE config files ' + @@ -437,7 +430,6 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { default: false, }, 'php-extension': sharedOptions['php-extension'], - 'php-extension-config': sharedOptions['php-extension-config'], 'experimental-unsafe-ide-integration': sharedOptions['experimental-unsafe-ide-integration'], 'skip-browser': { @@ -913,7 +905,6 @@ export interface RunCLIArgs { memcached?: boolean; xdebug?: boolean | XdebugOptions; phpExtension?: string[]; - phpExtensionConfig?: string[]; experimentalUnsafeIdeIntegration?: string[]; experimentalDevtools?: boolean; 'experimental-blueprints-v2-runner'?: boolean; diff --git a/packages/playground/cli/tests/php-extensions.spec.ts b/packages/playground/cli/tests/php-extensions.spec.ts index 35208b7aca5..3d20657bb16 100644 --- a/packages/playground/cli/tests/php-extensions.spec.ts +++ b/packages/playground/cli/tests/php-extensions.spec.ts @@ -1,10 +1,4 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { tmpdir } from 'node:os'; -import { - cliExtensionArgsToExtensionsArray, - readPHPExtensionConfig, -} from '../src/php-extensions'; +import { cliExtensionArgsToExtensionsArray } from '../src/php-extensions'; describe('CLI PHP extensions', () => { test('converts built-in extension flags to runtime extension requests', () => { @@ -41,56 +35,4 @@ describe('CLI PHP extensions', () => { }, ]); }); - - test('reads --php-extension-config JSON files', async () => { - const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); - const configPath = path.join(tempDir, 'extension.json'); - await writeFile( - configPath, - JSON.stringify({ - name: 'wp_mysql_parser', - source: { - format: 'url', - url: './dist/wp_mysql_parser-php8.4-jspi.so', - }, - iniEntries: { - 'wp_mysql_parser.mode': 'parser', - }, - }) - ); - - try { - expect(readPHPExtensionConfig(configPath)).toEqual({ - name: 'wp_mysql_parser', - source: { - format: 'url', - url: './dist/wp_mysql_parser-php8.4-jspi.so', - }, - iniEntries: { - 'wp_mysql_parser.mode': 'parser', - }, - }); - expect( - cliExtensionArgsToExtensionsArray({ - phpExtensionConfig: [configPath], - }) - ).toEqual([readPHPExtensionConfig(configPath)]); - } finally { - await rm(tempDir, { recursive: true, force: true }); - } - }); - - test('rejects config files without an external extension source', async () => { - const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); - const configPath = path.join(tempDir, 'extension.json'); - await writeFile(configPath, JSON.stringify({ name: 'broken' })); - - try { - expect(() => readPHPExtensionConfig(configPath)).toThrow( - 'Expected an object with a source field' - ); - } finally { - await rm(tempDir, { recursive: true, force: true }); - } - }); });