@php-wasm/universal 0.9.19 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ import { PHP } from './php';
2
+ export type PHPFactoryOptions = {
3
+ isPrimary: boolean;
4
+ };
5
+ export type PHPFactory = (options: PHPFactoryOptions) => Promise<PHP>;
6
+ export interface ProcessManagerOptions {
7
+ /**
8
+ * The maximum number of PHP instances that can exist at
9
+ * the same time.
10
+ */
11
+ maxPhpInstances?: number;
12
+ /**
13
+ * The number of milliseconds to wait for a PHP instance when
14
+ * we have reached the maximum number of PHP instances and
15
+ * cannot spawn a new one. If the timeout is reached, we assume
16
+ * all the PHP instances are deadlocked and a throw MaxPhpInstancesError.
17
+ *
18
+ * Default: 5000
19
+ */
20
+ timeout?: number;
21
+ /**
22
+ * The primary PHP instance that's never killed. This instance
23
+ * contains the reference filesystem used by all other PHP instances.
24
+ */
25
+ primaryPhp?: PHP;
26
+ /**
27
+ * A factory function used for spawning new PHP instances.
28
+ */
29
+ phpFactory?: PHPFactory;
30
+ }
31
+ export interface SpawnedPHP {
32
+ php: PHP;
33
+ reap: () => void;
34
+ }
35
+ export declare class MaxPhpInstancesError extends Error {
36
+ constructor(limit: number);
37
+ }
38
+ /**
39
+ * A PHP Process manager.
40
+ *
41
+ * Maintains:
42
+ * * A single "primary" PHP instance that's never killed – it contains the
43
+ * reference filesystem used by all other PHP instances.
44
+ * * A pool of disposable PHP instances that are spawned to handle a single
45
+ * request and reaped immediately after.
46
+ *
47
+ * When a new request comes in, PHPProcessManager yields the idle instance to handle it,
48
+ * and immediately starts initializing a new idle instance. In other words, for n concurrent
49
+ * requests, there are at most n+1 PHP instances running at the same time.
50
+ *
51
+ * A slight nuance is that the first idle instance is not initialized until the first
52
+ * concurrent request comes in. This is because many use-cases won't involve parallel
53
+ * requests and, for those, we can avoid eagerly spinning up a second PHP instance.
54
+ *
55
+ * This strategy is inspired by Cowboy, an Erlang HTTP server. Handling a single extra
56
+ * request can happen immediately, while handling multiple extra requests requires
57
+ * extra time to spin up a few PHP instances. This is a more resource-friendly tradeoff
58
+ * than keeping 5 idle instances at all times.
59
+ */
60
+ export declare class PHPProcessManager implements AsyncDisposable {
61
+ private primaryPhp?;
62
+ private primaryIdle;
63
+ private nextInstance;
64
+ /**
65
+ * All spawned PHP instances, including the primary PHP instance.
66
+ * Used for bookkeeping and reaping all instances on dispose.
67
+ */
68
+ private allInstances;
69
+ private phpFactory?;
70
+ private maxPhpInstances;
71
+ private semaphore;
72
+ constructor(options?: ProcessManagerOptions);
73
+ /**
74
+ * Get the primary PHP instance.
75
+ *
76
+ * If the primary PHP instance is not set, it will be spawned
77
+ * using the provided phpFactory.
78
+ *
79
+ * @throws {Error} when called twice before the first call is resolved.
80
+ */
81
+ getPrimaryPhp(): Promise<PHP>;
82
+ /**
83
+ * Get a PHP instance.
84
+ *
85
+ * It could be either the primary PHP instance, an idle disposable PHP instance,
86
+ * or a newly spawned PHP instance – depending on the resource availability.
87
+ *
88
+ * @throws {MaxPhpInstancesError} when the maximum number of PHP instances is reached
89
+ * and the waiting timeout is exceeded.
90
+ */
91
+ acquirePHPInstance(): Promise<SpawnedPHP>;
92
+ /**
93
+ * Initiated spawning of a new PHP instance.
94
+ * This function is synchronous on purpose – it needs to synchronously
95
+ * add the spawn promise to the allInstances array without waiting
96
+ * for PHP to spawn.
97
+ */
98
+ private spawn;
99
+ /**
100
+ * Actually acquires the lock and spawns a new PHP instance.
101
+ */
102
+ private doSpawn;
103
+ [Symbol.asyncDispose](): Promise<void>;
104
+ }
@@ -0,0 +1,219 @@
1
+ import { PHP } from './php';
2
+ import { PHPResponse } from './php-response';
3
+ import { PHPRequest } from './universal-php';
4
+ import { PHPFactoryOptions, PHPProcessManager } from './php-process-manager';
5
+ export type RewriteRule = {
6
+ match: RegExp;
7
+ replacement: string;
8
+ };
9
+ interface BaseConfiguration {
10
+ /**
11
+ * The directory in the PHP filesystem where the server will look
12
+ * for the files to serve. Default: `/var/www`.
13
+ */
14
+ documentRoot?: string;
15
+ /**
16
+ * Request Handler URL. Used to populate $_SERVER details like HTTP_HOST.
17
+ */
18
+ absoluteUrl?: string;
19
+ /**
20
+ * Rewrite rules
21
+ */
22
+ rewriteRules?: RewriteRule[];
23
+ }
24
+ export type PHPRequestHandlerFactoryArgs = PHPFactoryOptions & {
25
+ requestHandler: PHPRequestHandler;
26
+ };
27
+ export type PHPRequestHandlerConfiguration = BaseConfiguration & ({
28
+ /**
29
+ * PHPProcessManager is required because the request handler needs
30
+ * to make a decision for each request.
31
+ *
32
+ * Static assets are served using the primary PHP's filesystem, even
33
+ * when serving 100 static files concurrently. No new PHP interpreter
34
+ * is ever created as there's no need for it.
35
+ *
36
+ * Dynamic PHP requests, however, require grabbing an available PHP
37
+ * interpreter, and that's where the PHPProcessManager comes in.
38
+ */
39
+ processManager: PHPProcessManager;
40
+ } | {
41
+ phpFactory: (requestHandler: PHPRequestHandlerFactoryArgs) => Promise<PHP>;
42
+ /**
43
+ * The maximum number of PHP instances that can exist at
44
+ * the same time.
45
+ */
46
+ maxPhpInstances?: number;
47
+ });
48
+ /**
49
+ * Handles HTTP requests using PHP runtime as a backend.
50
+ *
51
+ * @public
52
+ * @example Use PHPRequestHandler implicitly with a new PHP instance:
53
+ * ```js
54
+ * import { PHP } from '../../../web/src/index.ts';
55
+ *
56
+ * const php = await PHP.load( '7.4', {
57
+ * requestHandler: {
58
+ * // PHP FS path to serve the files from:
59
+ * documentRoot: '/www',
60
+ *
61
+ * // Used to populate $_SERVER['SERVER_NAME'] etc.:
62
+ * absoluteUrl: 'http://127.0.0.1'
63
+ * }
64
+ * } );
65
+ *
66
+ * php.mkdirTree('/www');
67
+ * php.writeFile('/www/index.php', '<?php echo "Hi from PHP!"; ');
68
+ *
69
+ * const response = await php.request({ path: '/index.php' });
70
+ * console.log(response.text);
71
+ * // "Hi from PHP!"
72
+ * ```
73
+ *
74
+ * @example Explicitly create a PHPRequestHandler instance and run a PHP script:
75
+ * ```js
76
+ * import {
77
+ * loadPHPRuntime,
78
+ * PHP,
79
+ * PHPRequestHandler,
80
+ * getPHPLoaderModule,
81
+ * } from '@php-wasm/web';
82
+ *
83
+ * const runtime = await loadPHPRuntime( await getPHPLoaderModule('7.4') );
84
+ * const php = new PHP( runtime );
85
+ *
86
+ * php.mkdirTree('/www');
87
+ * php.writeFile('/www/index.php', '<?php echo "Hi from PHP!"; ');
88
+ *
89
+ * const server = new PHPRequestHandler(php, {
90
+ * // PHP FS path to serve the files from:
91
+ * documentRoot: '/www',
92
+ *
93
+ * // Used to populate $_SERVER['SERVER_NAME'] etc.:
94
+ * absoluteUrl: 'http://127.0.0.1'
95
+ * });
96
+ *
97
+ * const response = server.request({ path: '/index.php' });
98
+ * console.log(response.text);
99
+ * // "Hi from PHP!"
100
+ * ```
101
+ */
102
+ export declare class PHPRequestHandler {
103
+ #private;
104
+ rewriteRules: RewriteRule[];
105
+ processManager: PHPProcessManager;
106
+ /**
107
+ * The request handler needs to decide whether to serve a static asset or
108
+ * run the PHP interpreter. For static assets it should just reuse the primary
109
+ * PHP even if there's 50 concurrent requests to serve. However, for
110
+ * dynamic PHP requests, it needs to grab an available interpreter.
111
+ * Therefore, it cannot just accept PHP as an argument as serving requests
112
+ * requires access to ProcessManager.
113
+ *
114
+ * @param php - The PHP instance.
115
+ * @param config - Request Handler configuration.
116
+ */
117
+ constructor(config: PHPRequestHandlerConfiguration);
118
+ getPrimaryPhp(): Promise<PHP>;
119
+ /**
120
+ * Converts a path to an absolute URL based at the PHPRequestHandler
121
+ * root.
122
+ *
123
+ * @param path The server path to convert to an absolute URL.
124
+ * @returns The absolute URL.
125
+ */
126
+ pathToInternalUrl(path: string): string;
127
+ /**
128
+ * Converts an absolute URL based at the PHPRequestHandler to a relative path
129
+ * without the server pathname and scope.
130
+ *
131
+ * @param internalUrl An absolute URL based at the PHPRequestHandler root.
132
+ * @returns The relative path.
133
+ */
134
+ internalUrlToPath(internalUrl: string): string;
135
+ /**
136
+ * The absolute URL of this PHPRequestHandler instance.
137
+ */
138
+ get absoluteUrl(): string;
139
+ /**
140
+ * The directory in the PHP filesystem where the server will look
141
+ * for the files to serve. Default: `/var/www`.
142
+ */
143
+ get documentRoot(): string;
144
+ /**
145
+ * Serves the request – either by serving a static file, or by
146
+ * dispatching it to the PHP runtime.
147
+ *
148
+ * The request() method mode behaves like a web server and only works if
149
+ * the PHP was initialized with a `requestHandler` option (which the online version
150
+ * of WordPress Playground does by default).
151
+ *
152
+ * In the request mode, you pass an object containing the request information
153
+ * (method, headers, body, etc.) and the path to the PHP file to run:
154
+ *
155
+ * ```ts
156
+ * const php = PHP.load('7.4', {
157
+ * requestHandler: {
158
+ * documentRoot: "/www"
159
+ * }
160
+ * })
161
+ * php.writeFile("/www/index.php", `<?php echo file_get_contents("php://input");`);
162
+ * const result = await php.request({
163
+ * method: "GET",
164
+ * headers: {
165
+ * "Content-Type": "text/plain"
166
+ * },
167
+ * body: "Hello world!",
168
+ * path: "/www/index.php"
169
+ * });
170
+ * // result.text === "Hello world!"
171
+ * ```
172
+ *
173
+ * The `request()` method cannot be used in conjunction with `cli()`.
174
+ *
175
+ * @example
176
+ * ```js
177
+ * const output = await php.request({
178
+ * method: 'GET',
179
+ * url: '/index.php',
180
+ * headers: {
181
+ * 'X-foo': 'bar',
182
+ * },
183
+ * body: {
184
+ * foo: 'bar',
185
+ * },
186
+ * });
187
+ * console.log(output.stdout); // "Hello world!"
188
+ * ```
189
+ *
190
+ * @param request - PHP Request data.
191
+ */
192
+ request(request: PHPRequest): Promise<PHPResponse>;
193
+ }
194
+ /**
195
+ * Guesses whether the given path looks like a PHP file.
196
+ *
197
+ * @example
198
+ * ```js
199
+ * seemsLikeAPHPRequestHandlerPath('/index.php') // true
200
+ * seemsLikeAPHPRequestHandlerPath('/index.php') // true
201
+ * seemsLikeAPHPRequestHandlerPath('/index.php/foo/bar') // true
202
+ * seemsLikeAPHPRequestHandlerPath('/index.html') // false
203
+ * seemsLikeAPHPRequestHandlerPath('/index.html/foo/bar') // false
204
+ * seemsLikeAPHPRequestHandlerPath('/') // true
205
+ * ```
206
+ *
207
+ * @param path The path to check.
208
+ * @returns Whether the path seems like a PHP server path.
209
+ */
210
+ export declare function seemsLikeAPHPRequestHandlerPath(path: string): boolean;
211
+ /**
212
+ * Applies the given rewrite rules to the given path.
213
+ *
214
+ * @param path The path to apply the rules to.
215
+ * @param rules The rules to apply.
216
+ * @returns The path with the rules applied.
217
+ */
218
+ export declare function applyRewriteRules(path: string, rules: RewriteRule[]): string;
219
+ export {};
@@ -0,0 +1,55 @@
1
+ export interface PHPResponseData {
2
+ /**
3
+ * Response headers.
4
+ */
5
+ readonly headers: Record<string, string[]>;
6
+ /**
7
+ * Response body. Contains the output from `echo`,
8
+ * `print`, inline HTML etc.
9
+ */
10
+ readonly bytes: ArrayBuffer;
11
+ /**
12
+ * Stderr contents, if any.
13
+ */
14
+ readonly errors: string;
15
+ /**
16
+ * The exit code of the script. `0` is a success, while
17
+ * `1` and `2` indicate an error.
18
+ */
19
+ readonly exitCode: number;
20
+ /**
21
+ * Response HTTP status code, e.g. 200.
22
+ */
23
+ readonly httpStatusCode: number;
24
+ }
25
+ /**
26
+ * PHP response. Body is an `ArrayBuffer` because it can
27
+ * contain binary data.
28
+ *
29
+ * This type is used in Comlink.transferHandlers.set('PHPResponse', \{ ... \})
30
+ * so be sure to update that if you change this type.
31
+ */
32
+ export declare class PHPResponse implements PHPResponseData {
33
+ /** @inheritDoc */
34
+ readonly headers: Record<string, string[]>;
35
+ /** @inheritDoc */
36
+ readonly bytes: ArrayBuffer;
37
+ /** @inheritDoc */
38
+ readonly errors: string;
39
+ /** @inheritDoc */
40
+ readonly exitCode: number;
41
+ /** @inheritDoc */
42
+ readonly httpStatusCode: number;
43
+ constructor(httpStatusCode: number, headers: Record<string, string[]>, body: ArrayBuffer, errors?: string, exitCode?: number);
44
+ static forHttpCode(httpStatusCode: number, text?: string): PHPResponse;
45
+ static fromRawData(data: PHPResponseData): PHPResponse;
46
+ toRawData(): PHPResponseData;
47
+ /**
48
+ * Response body as JSON.
49
+ */
50
+ get json(): any;
51
+ /**
52
+ * Response body as text.
53
+ */
54
+ get text(): string;
55
+ }
@@ -0,0 +1,77 @@
1
+ import { EmscriptenDownloadMonitor } from '../../../progress/src/index.ts';
2
+ import { PHP } from './php';
3
+ import { PHPRequestHandler } from './php-request-handler';
4
+ import { PHPResponse } from './php-response';
5
+ import { PHPRequest, PHPRunOptions, MessageListener, PHPEvent, PHPEventListener } from './universal-php';
6
+ import { RmDirOptions, ListFilesOptions } from './fs-helpers';
7
+ export type LimitedPHPApi = Pick<PHP, 'request' | 'defineConstant' | 'addEventListener' | 'removeEventListener' | 'mkdir' | 'mkdirTree' | 'readFileAsText' | 'readFileAsBuffer' | 'writeFile' | 'unlink' | 'mv' | 'rmdir' | 'listFiles' | 'isDir' | 'fileExists' | 'chdir' | 'run' | 'onMessage'> & {
8
+ documentRoot: PHP['documentRoot'];
9
+ absoluteUrl: PHP['absoluteUrl'];
10
+ };
11
+ /**
12
+ * A PHP client that can be used to run PHP code in the browser.
13
+ */
14
+ export declare class PHPWorker implements LimitedPHPApi {
15
+ /** @inheritDoc @php-wasm/universal!RequestHandler.absoluteUrl */
16
+ absoluteUrl: string;
17
+ /** @inheritDoc @php-wasm/universal!RequestHandler.documentRoot */
18
+ documentRoot: string;
19
+ /** @inheritDoc */
20
+ constructor(requestHandler?: PHPRequestHandler, monitor?: EmscriptenDownloadMonitor);
21
+ __internal_setRequestHandler(requestHandler: PHPRequestHandler): void;
22
+ /**
23
+ * @internal
24
+ * @deprecated
25
+ * Do not use this method directly in the code consuming
26
+ * the web API. It will change or even be removed without
27
+ * a warning.
28
+ */
29
+ protected __internal_getPHP(): PHP | undefined;
30
+ setPrimaryPHP(php: PHP): Promise<void>;
31
+ /** @inheritDoc @php-wasm/universal!PHPRequestHandler.pathToInternalUrl */
32
+ pathToInternalUrl(path: string): string;
33
+ /** @inheritDoc @php-wasm/universal!PHPRequestHandler.internalUrlToPath */
34
+ internalUrlToPath(internalUrl: string): string;
35
+ /**
36
+ * The onDownloadProgress event listener.
37
+ */
38
+ onDownloadProgress(callback: (progress: CustomEvent<ProgressEvent>) => void): Promise<void>;
39
+ /** @inheritDoc @php-wasm/universal!PHP.mv */
40
+ mv(fromPath: string, toPath: string): Promise<void>;
41
+ /** @inheritDoc @php-wasm/universal!PHP.rmdir */
42
+ rmdir(path: string, options?: RmDirOptions): Promise<void>;
43
+ /** @inheritDoc @php-wasm/universal!PHPRequestHandler.request */
44
+ request(request: PHPRequest): Promise<PHPResponse>;
45
+ /** @inheritDoc @php-wasm/universal!/PHP.run */
46
+ run(request: PHPRunOptions): Promise<PHPResponse>;
47
+ /** @inheritDoc @php-wasm/universal!/PHP.chdir */
48
+ chdir(path: string): void;
49
+ /** @inheritDoc @php-wasm/universal!/PHP.setSapiName */
50
+ setSapiName(newName: string): void;
51
+ /** @inheritDoc @php-wasm/universal!/PHP.mkdir */
52
+ mkdir(path: string): void;
53
+ /** @inheritDoc @php-wasm/universal!/PHP.mkdirTree */
54
+ mkdirTree(path: string): void;
55
+ /** @inheritDoc @php-wasm/universal!/PHP.readFileAsText */
56
+ readFileAsText(path: string): string;
57
+ /** @inheritDoc @php-wasm/universal!/PHP.readFileAsBuffer */
58
+ readFileAsBuffer(path: string): Uint8Array;
59
+ /** @inheritDoc @php-wasm/universal!/PHP.writeFile */
60
+ writeFile(path: string, data: string | Uint8Array): void;
61
+ /** @inheritDoc @php-wasm/universal!/PHP.unlink */
62
+ unlink(path: string): void;
63
+ /** @inheritDoc @php-wasm/universal!/PHP.listFiles */
64
+ listFiles(path: string, options?: ListFilesOptions): string[];
65
+ /** @inheritDoc @php-wasm/universal!/PHP.isDir */
66
+ isDir(path: string): boolean;
67
+ /** @inheritDoc @php-wasm/universal!/PHP.fileExists */
68
+ fileExists(path: string): boolean;
69
+ /** @inheritDoc @php-wasm/universal!/PHP.onMessage */
70
+ onMessage(listener: MessageListener): void;
71
+ /** @inheritDoc @php-wasm/universal!/PHP.defineConstant */
72
+ defineConstant(key: string, value: string | boolean | number | null): void;
73
+ /** @inheritDoc @php-wasm/universal!/PHP.addEventListener */
74
+ addEventListener(eventType: PHPEvent['type'], listener: PHPEventListener): void;
75
+ /** @inheritDoc @php-wasm/universal!/PHP.removeEventListener */
76
+ removeEventListener(eventType: PHPEvent['type'], listener: PHPEventListener): void;
77
+ }