@wp-operations/wp-app 0.1.1 → 0.3.0

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.
Files changed (4) hide show
  1. package/README.md +24 -14
  2. package/dist/index.js +407 -496
  3. package/dist/main.js +504 -581
  4. package/package.json +5 -10
package/dist/main.js CHANGED
@@ -1,95 +1,193 @@
1
1
  // src/run-cli.ts
2
- import fs11 from "fs-extra";
3
- import path14 from "path";
2
+ import fs13 from "fs";
3
+ import path11 from "path";
4
4
  import yargs from "yargs";
5
5
  import { hideBin } from "yargs/helpers";
6
6
  import { spawn, execSync } from "child_process";
7
7
 
8
8
  // src/start-server.ts
9
- import fs10 from "fs";
9
+ import fs11 from "fs";
10
+ import { Readable } from "stream";
11
+ import { pipeline } from "stream/promises";
10
12
  import express from "express";
11
- import fileUpload from "express-fileupload";
12
13
 
13
- // src/port-finder.ts
14
- import http from "http";
14
+ // src/engine.ts
15
+ import fs10 from "fs";
16
+ import path9 from "path";
17
+ import { createNodeFsMountHandler } from "@php-wasm/node";
18
+
19
+ // src/boot.ts
20
+ import { rootCertificates } from "tls";
21
+ import { loadNodeRuntime } from "@php-wasm/node";
22
+ import { PHP, PHPRequestHandler, setPhpIniEntries } from "@php-wasm/universal";
23
+ var CA_BUNDLE_PATH = "/internal/ca-bundle.crt";
24
+ async function createPhp(phpVersion) {
25
+ const php = new PHP(
26
+ await loadNodeRuntime(phpVersion, {
27
+ emscriptenOptions: { processId: process.pid }
28
+ })
29
+ );
30
+ php.setSapiName("cli");
31
+ php.mkdir("/internal");
32
+ php.writeFile(CA_BUNDLE_PATH, rootCertificates.join("\n"));
33
+ await setPhpIniEntries(php, {
34
+ "openssl.cafile": CA_BUNDLE_PATH,
35
+ "curl.cainfo": CA_BUNDLE_PATH,
36
+ allow_url_fopen: "1",
37
+ disable_functions: ""
38
+ });
39
+ return php;
40
+ }
41
+ function createRequestHandler(php, documentRoot, absoluteUrl) {
42
+ return new PHPRequestHandler({ php, documentRoot, absoluteUrl });
43
+ }
44
+ async function runPhp(php, code) {
45
+ const response = await php.run({ code: `<?php ${code}` });
46
+ if (response.exitCode !== 0) {
47
+ throw new Error(response.errors || `PHP exited ${response.exitCode}`);
48
+ }
49
+ return response;
50
+ }
51
+ async function unzipTo(php, zip, destination) {
52
+ const tmpZip = `/internal/upload-${Date.now()}.zip`;
53
+ php.writeFile(tmpZip, new Uint8Array(await zip.arrayBuffer()));
54
+ await runPhp(
55
+ php,
56
+ `
57
+ $zip = new ZipArchive();
58
+ if ($zip->open(${str(tmpZip)}) !== true) exit(1);
59
+ if (!is_dir(${str(destination)})) mkdir(${str(destination)}, 0777, true);
60
+ if (!$zip->extractTo(${str(destination)})) exit(1);
61
+ $zip->close();
62
+ unlink(${str(tmpZip)});
63
+ `
64
+ );
65
+ }
66
+ async function installCore(php, coreZip, docroot) {
67
+ const staging = `${docroot}/.wpapp-staging`;
68
+ await unzipTo(php, coreZip, staging);
69
+ await runPhp(
70
+ php,
71
+ `
72
+ function wpapp_core_root($dir) {
73
+ if (file_exists("$dir/wp-config-sample.php")) return $dir;
74
+ foreach (scandir($dir) as $entry) {
75
+ if ($entry === '.' || $entry === '..') continue;
76
+ $candidate = "$dir/$entry";
77
+ if (is_dir($candidate) && file_exists("$candidate/wp-config-sample.php")) {
78
+ return $candidate;
79
+ }
80
+ }
81
+ exit(1);
82
+ }
83
+ function wpapp_rrmdir($dir) {
84
+ foreach (scandir($dir) as $entry) {
85
+ if ($entry === '.' || $entry === '..') continue;
86
+ $path = "$dir/$entry";
87
+ is_dir($path) ? wpapp_rrmdir($path) : unlink($path);
88
+ }
89
+ rmdir($dir);
90
+ }
91
+ $root = wpapp_core_root(${str(staging)});
92
+ foreach (scandir($root) as $entry) {
93
+ if ($entry === '.' || $entry === '..') continue;
94
+ rename("$root/$entry", ${str(docroot)} . "/$entry");
95
+ }
96
+ wpapp_rrmdir(${str(staging)});
97
+ if (!file_exists(${str(docroot)} . '/wp-config.php')) {
98
+ copy(
99
+ ${str(docroot)} . '/wp-config-sample.php',
100
+ ${str(docroot)} . '/wp-config.php'
101
+ );
102
+ }
103
+ `
104
+ );
105
+ }
106
+ async function installSqliteIntegration(php, pluginZip, docroot) {
107
+ const pluginDir = `${docroot}/wp-content/plugins/sqlite-database-integration`;
108
+ const staging = `${docroot}/wp-content/.wpapp-sqlite-staging`;
109
+ await unzipTo(php, pluginZip, staging);
110
+ await runPhp(
111
+ php,
112
+ `
113
+ $root = ${str(staging)};
114
+ $entries = array_values(array_diff(scandir($root), ['.', '..']));
115
+ if (count($entries) === 1 && is_dir("$root/$entries[0]")) {
116
+ $root = "$root/$entries[0]";
117
+ }
118
+ if (!is_dir(${str(pluginDir)})) {
119
+ rename($root, ${str(pluginDir)});
120
+ }
121
+ if (is_dir(${str(staging)})) {
122
+ @rmdir(${str(staging)});
123
+ }
124
+ $dropIn = file_get_contents(${str(pluginDir)} . '/db.copy');
125
+ $dropIn = str_replace(
126
+ '{SQLITE_IMPLEMENTATION_FOLDER_PATH}',
127
+ ${str(pluginDir)},
128
+ $dropIn
129
+ );
130
+ $dropIn = str_replace(
131
+ '{SQLITE_PLUGIN}',
132
+ 'sqlite-database-integration/load.php',
133
+ $dropIn
134
+ );
135
+ file_put_contents(${str(docroot)} . '/wp-content/db.php', $dropIn);
136
+ `
137
+ );
138
+ }
139
+ function defineSiteConstants(php, siteUrl, databaseDir) {
140
+ php.defineConstant("WP_HOME", siteUrl);
141
+ php.defineConstant("WP_SITEURL", siteUrl);
142
+ php.defineConstant("DB_DIR", databaseDir);
143
+ php.defineConstant("DB_FILE", ".ht.sqlite");
144
+ php.defineConstant("WP_SQLITE_AST_DRIVER", true);
145
+ }
146
+ async function runInstaller(requestHandler) {
147
+ const fields = new URLSearchParams({
148
+ language: "en",
149
+ prefix: "wp_",
150
+ weblog_title: "My WordPress Website",
151
+ user_name: "admin",
152
+ admin_password: "password",
153
+ admin_password2: "password",
154
+ Submit: "Install WordPress",
155
+ pw_weak: "1",
156
+ admin_email: "admin@localhost.com"
157
+ });
158
+ return requestHandler.request({
159
+ url: "/wp-admin/install.php?step=2",
160
+ method: "POST",
161
+ headers: { "content-type": "application/x-www-form-urlencoded" },
162
+ body: new TextEncoder().encode(fields.toString())
163
+ });
164
+ }
165
+ function str(value) {
166
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
167
+ }
15
168
 
16
169
  // src/constants.ts
17
- var WP_NOW_HIDDEN_FOLDER = ".wp-app";
18
- var SQLITE_FILENAME = "sqlite-database-integration";
170
+ var WP_APP_HIDDEN_FOLDER = ".wp-app";
171
+ var WP_APP_HOME_ENV = "WP_APP_HOME";
19
172
  var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
20
173
  var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
21
174
  var DEFAULT_PORT = 8881;
22
- var DEFAULT_PHP_VERSION = "8.0";
175
+ var DEFAULT_PHP_VERSION = "8.3";
23
176
  var DEFAULT_WORDPRESS_VERSION = "latest";
24
-
25
- // src/port-finder.ts
26
- var PortFinder = class _PortFinder {
27
- static #instance;
28
- #searchPort = DEFAULT_PORT;
29
- #openPort = null;
30
- constructor() {
31
- }
32
- static getInstance() {
33
- if (!_PortFinder.#instance) {
34
- _PortFinder.#instance = new _PortFinder();
35
- }
36
- return _PortFinder.#instance;
37
- }
38
- #incrementPort() {
39
- return this.#searchPort++;
40
- }
41
- #isPortFree() {
42
- return new Promise((resolve) => {
43
- const server = http.createServer();
44
- server.listen(this.#searchPort, () => {
45
- server.close();
46
- resolve(true);
47
- }).on("error", () => {
48
- resolve(false);
49
- });
50
- });
51
- }
52
- /**
53
- * Returns the first available open port, caching and reusing it for subsequent calls.
54
- *
55
- * @returns {Promise<number>} A promise that resolves to the open port number.
56
- */
57
- async getOpenPort() {
58
- if (this.#openPort) {
59
- return this.#openPort;
60
- }
61
- while (!await this.#isPortFree()) {
62
- this.#incrementPort();
63
- }
64
- this.#openPort = this.#searchPort;
65
- return this.#openPort;
66
- }
67
- setPort(port) {
68
- this.#openPort = port;
69
- }
70
- };
71
- var portFinder = PortFinder.getInstance();
72
-
73
- // src/wp-now.ts
74
- import fs9 from "fs-extra";
75
- import { NodePHP } from "@php-wasm/node";
76
- import path13 from "path";
177
+ var DOCROOT = "/wordpress";
77
178
 
78
179
  // src/download.ts
79
- import fs8 from "fs-extra";
80
- import path11 from "path";
81
- import followRedirects from "follow-redirects";
82
- import unzipper from "unzipper";
83
- import os3 from "os";
180
+ import fs9 from "fs";
181
+ import path8 from "path";
84
182
 
85
- // src/wp-playground-wordpress/has-index-file.ts
86
- import fs from "fs-extra";
183
+ // src/detect/has-index-file.ts
184
+ import fs from "fs";
87
185
  import path from "path";
88
186
  function hasIndexFile(projectPath) {
89
187
  return fs.existsSync(path.join(projectPath, "index.php"));
90
188
  }
91
189
 
92
- // src/wp-playground-wordpress/is-valid-wordpress-version.ts
190
+ // src/detect/is-valid-wordpress-version.ts
93
191
  function isValidWordPressVersion(version) {
94
192
  const versionPattern = /^latest$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
95
193
  const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?)?$/;
@@ -99,12 +197,12 @@ function isClassicPressVersion(version) {
99
197
  return version === "classicpress" || version.startsWith("classicpress-");
100
198
  }
101
199
 
102
- // src/wp-playground-wordpress/get-plugin-file.ts
103
- import fs3 from "fs-extra";
200
+ // src/detect/get-plugin-file.ts
201
+ import fs3 from "fs";
104
202
  import path2, { basename } from "path";
105
203
 
106
- // src/wp-playground-wordpress/read-file-head.ts
107
- import fs2 from "fs-extra";
204
+ // src/detect/read-file-head.ts
205
+ import fs2 from "fs";
108
206
  function readFileHead(filePath, length = 8192) {
109
207
  const buffer = Buffer.alloc(length);
110
208
  const fd = fs2.openSync(filePath, "r");
@@ -114,7 +212,7 @@ function readFileHead(filePath, length = 8192) {
114
212
  return fileContentBuffer.toString();
115
213
  }
116
214
 
117
- // src/wp-playground-wordpress/get-plugin-file.ts
215
+ // src/detect/get-plugin-file.ts
118
216
  function heuristicSort(files, projectPath) {
119
217
  const heuristicsBestGuess = `${basename(projectPath)}.php`;
120
218
  const heuristicsBestGuessIndex = files.indexOf(heuristicsBestGuess);
@@ -138,14 +236,14 @@ function getPluginFile(projectPath) {
138
236
  return null;
139
237
  }
140
238
 
141
- // src/wp-playground-wordpress/is-plugin-directory.ts
239
+ // src/detect/is-plugin-directory.ts
142
240
  function isPluginDirectory(projectPath) {
143
241
  const pluginFile = getPluginFile(projectPath);
144
242
  return pluginFile !== null;
145
243
  }
146
244
 
147
- // src/wp-playground-wordpress/is-theme-directory.ts
148
- import fs4 from "fs-extra";
245
+ // src/detect/is-theme-directory.ts
246
+ import fs4 from "fs";
149
247
  import path3 from "path";
150
248
  function isThemeDirectory(projectPath) {
151
249
  const styleCSSExists = fs4.existsSync(path3.join(projectPath, "style.css"));
@@ -157,8 +255,8 @@ function isThemeDirectory(projectPath) {
157
255
  return themeNameRegex.test(styleCSS);
158
256
  }
159
257
 
160
- // src/wp-playground-wordpress/is-wp-content-directory.ts
161
- import fs5 from "fs-extra";
258
+ // src/detect/is-wp-content-directory.ts
259
+ import fs5 from "fs";
162
260
  import path4 from "path";
163
261
  function isWpContentDirectory(projectPath) {
164
262
  const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
@@ -170,15 +268,15 @@ function isWpContentDirectory(projectPath) {
170
268
  return false;
171
269
  }
172
270
 
173
- // src/wp-playground-wordpress/is-wordpress-directory.ts
174
- import fs6 from "fs-extra";
271
+ // src/detect/is-wordpress-directory.ts
272
+ import fs6 from "fs";
175
273
  import path5 from "path";
176
274
  function isWordPressDirectory(projectPath) {
177
275
  return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
178
276
  }
179
277
 
180
- // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
181
- import fs7 from "fs-extra";
278
+ // src/detect/is-wordpress-develop-directory.ts
279
+ import fs7 from "fs";
182
280
  import path6 from "path";
183
281
  function isWordPressDevelopDirectory(projectPath) {
184
282
  const requiredFiles = [
@@ -201,44 +299,30 @@ function shouldOutput() {
201
299
  return process.env.NODE_ENV !== "test";
202
300
  }
203
301
  var output = shouldOutput() ? console : null;
204
- function disableOutput() {
205
- output = null;
206
- }
207
302
 
208
- // src/get-wp-now-path.ts
209
- import path8 from "path";
210
- import os2 from "os";
211
-
212
- // src/get-wp-now-tmp-path.ts
213
- import path7 from "path";
303
+ // src/paths.ts
304
+ import crypto from "crypto";
305
+ import fs8 from "fs";
214
306
  import os from "os";
215
- function getWpNowTmpPath() {
216
- const tmpDirectory = os.tmpdir();
217
- return path7.join(tmpDirectory, `wp-now-tests-hidden-folder`);
307
+ import path7 from "path";
308
+ function getWpAppHome() {
309
+ return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
218
310
  }
219
-
220
- // src/get-wp-now-path.ts
221
- function getWpNowPath() {
222
- if (process.env.NODE_ENV !== "test") {
223
- return path8.join(os2.homedir(), WP_NOW_HIDDEN_FOLDER);
224
- }
225
- return getWpNowTmpPath();
311
+ function getCachePath() {
312
+ return ensureDir(path7.join(getWpAppHome(), "cache"));
226
313
  }
227
-
228
- // src/get-wordpress-versions-path.ts
229
- import path9 from "path";
230
- function getWordpressVersionsPath() {
231
- return path9.join(getWpNowPath(), "wordpress-versions");
314
+ function getSitePath(projectPath) {
315
+ const projectName = path7.basename(path7.resolve(projectPath));
316
+ const hash = crypto.createHash("sha1").update(path7.resolve(projectPath)).digest("hex");
317
+ return path7.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
232
318
  }
233
-
234
- // src/get-sqlite-path.ts
235
- import path10 from "path";
236
- function getSqlitePath() {
237
- return path10.join(getWpNowPath(), `${SQLITE_FILENAME}-main`);
319
+ function ensureDir(dir) {
320
+ fs8.mkdirSync(dir, { recursive: true });
321
+ return dir;
238
322
  }
239
323
 
240
324
  // src/download.ts
241
- function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) {
325
+ function getWordPressVersionUrl(version) {
242
326
  if (!isValidWordPressVersion(version)) {
243
327
  throw new Error(
244
328
  'Unrecognized WordPress version. Please use "latest", numeric versions such as "6.2", "6.0.1", "6.2-beta1", "6.2-RC1", or "classicpress" / "classicpress-2.4.1".'
@@ -253,129 +337,35 @@ function getClassicPressVersionUrl(version) {
253
337
  const tag = version.slice("classicpress-".length);
254
338
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
255
339
  }
256
- followRedirects.maxRedirects = 5;
257
- var { https } = followRedirects;
258
- async function downloadFileAndUnzip({
259
- url,
260
- destinationFolder,
261
- checkFinalPath,
262
- itemName
263
- }) {
264
- if (fs8.existsSync(checkFinalPath)) {
265
- output?.log(`${itemName} folder already exists. Skipping download.`);
266
- return { downloaded: false, statusCode: 0 };
267
- }
268
- let statusCode = 0;
269
- try {
270
- fs8.ensureDirSync(path11.dirname(destinationFolder));
271
- output?.log(`Downloading ${itemName}...`);
272
- const response = await new Promise(
273
- (resolve) => https.get(url, (response2) => resolve(response2))
274
- );
275
- statusCode = response.statusCode;
276
- if (response.statusCode !== 200) {
340
+ async function cachedZip(url, cacheKey) {
341
+ const cacheFile = path8.join(getCachePath(), cacheKey);
342
+ if (!fs9.existsSync(cacheFile)) {
343
+ output?.log(`Downloading ${cacheKey}...`);
344
+ const response = await fetch(url, { redirect: "follow" });
345
+ if (!response.ok) {
277
346
  throw new Error(
278
- `Failed to download file (Status code ${response.statusCode}).`
347
+ `Failed to download ${url} (HTTP ${response.status}).`
279
348
  );
280
349
  }
281
- await response.pipe(unzipper.Parse()).on("entry", (entry) => {
282
- const filePath = path11.join(destinationFolder, entry.path);
283
- fs8.ensureDirSync(path11.dirname(filePath));
284
- if (entry.type !== "Directory") {
285
- entry.pipe(fs8.createWriteStream(filePath));
286
- }
287
- }).promise();
288
- return { downloaded: true, statusCode };
289
- } catch (err) {
290
- output?.error(`Error downloading or unzipping ${itemName}:`, err);
291
- }
292
- return { downloaded: false, statusCode };
293
- }
294
- async function downloadWordPress(wordPressVersion = DEFAULT_WORDPRESS_VERSION) {
295
- const finalFolder = path11.join(getWordpressVersionsPath(), wordPressVersion);
296
- if (isClassicPressVersion(wordPressVersion)) {
297
- return downloadClassicPress(wordPressVersion, finalFolder);
298
- }
299
- const tempFolder = os3.tmpdir();
300
- const { downloaded, statusCode } = await downloadFileAndUnzip({
301
- url: getWordPressVersionUrl(wordPressVersion),
302
- destinationFolder: tempFolder,
303
- checkFinalPath: finalFolder,
304
- itemName: `WordPress ${wordPressVersion}`
305
- });
306
- if (downloaded) {
307
- fs8.ensureDirSync(path11.dirname(finalFolder));
308
- fs8.moveSync(path11.join(tempFolder, "wordpress"), finalFolder, {
309
- overwrite: true
310
- });
311
- } else if (404 === statusCode) {
312
- output?.log(
313
- `WordPress ${wordPressVersion} not found. Check https://wordpress.org/download/releases/ for available versions.`
314
- );
315
- process.exit(1);
350
+ const bytes = Buffer.from(await response.arrayBuffer());
351
+ fs9.writeFileSync(`${cacheFile}.partial`, bytes);
352
+ fs9.renameSync(`${cacheFile}.partial`, cacheFile);
353
+ output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
316
354
  }
355
+ return new File([fs9.readFileSync(cacheFile)], cacheKey);
317
356
  }
318
- async function downloadClassicPress(version, finalFolder) {
319
- const tempFolder = path11.join(os3.tmpdir(), `wp-app-classicpress-${Date.now()}`);
320
- const { downloaded, statusCode } = await downloadFileAndUnzip({
321
- url: getClassicPressVersionUrl(version),
322
- destinationFolder: tempFolder,
323
- checkFinalPath: finalFolder,
324
- itemName: `ClassicPress ${version}`
325
- });
326
- if (downloaded) {
327
- fs8.ensureDirSync(path11.dirname(finalFolder));
328
- const entries = fs8.readdirSync(tempFolder);
329
- const singleDirRoot = entries.length === 1 && fs8.statSync(path11.join(tempFolder, entries[0])).isDirectory() ? path11.join(tempFolder, entries[0]) : null;
330
- fs8.moveSync(singleDirRoot ?? tempFolder, finalFolder, {
331
- overwrite: true
332
- });
333
- fs8.removeSync(tempFolder);
334
- } else if (404 === statusCode) {
335
- output?.log(
336
- `ClassicPress ${version} not found. Check https://www.classicpress.net for available releases.`
337
- );
338
- process.exit(1);
357
+ async function getCoreZip(version) {
358
+ if (isClassicPressVersion(version)) {
359
+ return cachedZip(getClassicPressVersionUrl(version), `${version}.zip`);
339
360
  }
361
+ return cachedZip(getWordPressVersionUrl(version), `wordpress-${version}.zip`);
340
362
  }
341
- async function downloadSqliteIntegrationPlugin() {
342
- return downloadFileAndUnzip({
343
- url: SQLITE_URL,
344
- destinationFolder: getWpNowPath(),
345
- checkFinalPath: getSqlitePath(),
346
- itemName: "SQLite"
347
- });
348
- }
349
- async function downloadMuPlugins() {
350
- fs8.ensureDirSync(path11.join(getWpNowPath(), "mu-plugins"));
351
- fs8.writeFile(
352
- path11.join(getWpNowPath(), "mu-plugins", "0-allow-wp-org.php"),
353
- `<?php
354
- // Needed because gethostbyname( 'wordpress.org' ) returns
355
- // a private network IP address for some reason.
356
- add_filter( 'allowed_redirect_hosts', function( $deprecated = '' ) {
357
- return array(
358
- 'wordpress.org',
359
- 'api.wordpress.org',
360
- 'downloads.wordpress.org',
361
- );
362
- } );`
363
- );
363
+ async function getSqliteIntegrationZip() {
364
+ return cachedZip(SQLITE_URL, "sqlite-database-integration.zip");
364
365
  }
365
366
 
366
- // src/wp-now.ts
367
- import {
368
- activatePlugin,
369
- activateTheme,
370
- defineWpConfigConsts,
371
- login
372
- } from "@wp-playground/blueprints";
373
-
374
367
  // src/config.ts
375
- import {
376
- SupportedPHPVersionsList
377
- } from "@php-wasm/universal";
378
- import crypto from "crypto";
368
+ import { SupportedPHPVersionsList } from "@php-wasm/universal";
379
369
 
380
370
  // src/github-codespaces.ts
381
371
  var isGitHubCodespace = Boolean(
@@ -385,15 +375,61 @@ function getCodeSpaceURL(port) {
385
375
  return `https://${process.env.CODESPACE_NAME}-${port}.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
386
376
  }
387
377
 
378
+ // src/port-finder.ts
379
+ import http from "http";
380
+ var PortFinder = class _PortFinder {
381
+ static #instance;
382
+ #searchPort = DEFAULT_PORT;
383
+ #openPort = null;
384
+ constructor() {
385
+ }
386
+ static getInstance() {
387
+ if (!_PortFinder.#instance) {
388
+ _PortFinder.#instance = new _PortFinder();
389
+ }
390
+ return _PortFinder.#instance;
391
+ }
392
+ #incrementPort() {
393
+ return this.#searchPort++;
394
+ }
395
+ #isPortFree() {
396
+ return new Promise((resolve) => {
397
+ const server = http.createServer();
398
+ server.listen(this.#searchPort, () => {
399
+ server.close();
400
+ resolve(true);
401
+ }).on("error", () => {
402
+ resolve(false);
403
+ });
404
+ });
405
+ }
406
+ /**
407
+ * Returns the first available open port, caching and reusing it for subsequent calls.
408
+ *
409
+ * @returns {Promise<number>} A promise that resolves to the open port number.
410
+ */
411
+ async getOpenPort() {
412
+ if (this.#openPort) {
413
+ return this.#openPort;
414
+ }
415
+ while (!await this.#isPortFree()) {
416
+ this.#incrementPort();
417
+ }
418
+ this.#openPort = this.#searchPort;
419
+ return this.#openPort;
420
+ }
421
+ setPort(port) {
422
+ this.#openPort = port;
423
+ }
424
+ };
425
+ var portFinder = PortFinder.getInstance();
426
+
388
427
  // src/config.ts
389
- import path12 from "path";
390
428
  var DEFAULT_OPTIONS = {
391
429
  phpVersion: DEFAULT_PHP_VERSION,
392
430
  wordPressVersion: DEFAULT_WORDPRESS_VERSION,
393
- documentRoot: "/var/www/html",
394
431
  projectPath: process.cwd(),
395
- mode: "auto" /* AUTO */,
396
- numberOfPhpInstances: 1
432
+ mode: "auto" /* AUTO */
397
433
  };
398
434
  async function getAbsoluteURL() {
399
435
  const port = await portFinder.getOpenPort();
@@ -402,13 +438,7 @@ async function getAbsoluteURL() {
402
438
  }
403
439
  return `http://localhost:${port}`;
404
440
  }
405
- function getWpContentHomePath(projectPath, mode) {
406
- const basename2 = path12.basename(projectPath);
407
- const directoryHash = crypto.createHash("sha1").update(projectPath).digest("hex");
408
- const projectDirectory = mode === "playground" /* PLAYGROUND */ ? "playground" : `${basename2}-${directoryHash}`;
409
- return path12.join(getWpNowPath(), "wp-content", projectDirectory);
410
- }
411
- async function getWpNowConfig(args) {
441
+ async function getWpAppConfig(args) {
412
442
  if (args.port) {
413
443
  portFinder.setPort(args.port);
414
444
  }
@@ -431,12 +461,6 @@ async function getWpNowConfig(args) {
431
461
  if (!options.mode || options.mode === "auto") {
432
462
  options.mode = inferMode(options.projectPath);
433
463
  }
434
- if (!options.wpContentPath) {
435
- options.wpContentPath = getWpContentHomePath(
436
- options.projectPath,
437
- options.mode
438
- );
439
- }
440
464
  if (!options.absoluteUrl) {
441
465
  options.absoluteUrl = await getAbsoluteURL();
442
466
  }
@@ -453,241 +477,154 @@ async function getWpNowConfig(args) {
453
477
  return options;
454
478
  }
455
479
 
456
- // src/wp-now.ts
457
- function seemsLikeAPHPFile(path15) {
458
- return path15.endsWith(".php") || path15.includes(".php/");
459
- }
460
- async function applyToInstances(phpInstances, callback) {
461
- for (let i = 0; i < phpInstances.length; i++) {
462
- await callback(phpInstances[i]);
480
+ // src/engine.ts
481
+ async function startWPApp(options) {
482
+ const projectPath = path9.resolve(options.projectPath);
483
+ const mode = options.mode;
484
+ output?.log(`directory: ${options.projectPath}`);
485
+ output?.log(`mode: ${mode}`);
486
+ output?.log(`php: ${options.phpVersion}`);
487
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
488
+ const isWordPressBacked = mode !== "index" /* INDEX */;
489
+ const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
490
+ if (isWordPressBacked) {
491
+ output?.log(`wp: ${options.wordPressVersion}`);
492
+ output?.log(`site data: ${hostDocroot}`);
493
+ }
494
+ const php = await createPhp(options.phpVersion);
495
+ php.mkdir(DOCROOT);
496
+ await php.mount(DOCROOT, createNodeFsMountHandler(hostDocroot));
497
+ const requestHandler = createRequestHandler(
498
+ php,
499
+ DOCROOT,
500
+ options.absoluteUrl
501
+ );
502
+ if (freshSite) {
503
+ await installCore(
504
+ php,
505
+ await getCoreZip(options.wordPressVersion),
506
+ DOCROOT
507
+ );
463
508
  }
464
- }
465
- async function startWPNow(options = {}) {
466
- const { documentRoot } = options;
467
- const nodePHPOptions = {
468
- requestHandler: {
469
- documentRoot,
470
- absoluteUrl: options.absoluteUrl,
471
- isStaticFilePath: (path15) => {
472
- try {
473
- const fullPath = options.documentRoot + path15;
474
- return php.fileExists(fullPath) && !php.isDir(fullPath) && !seemsLikeAPHPFile(fullPath);
475
- } catch (e) {
476
- output?.error(e);
477
- return false;
478
- }
479
- }
509
+ if (isWordPressBacked) {
510
+ if (!php.fileExists(`${DOCROOT}/wp-content/db.php`)) {
511
+ await installSqliteIntegration(
512
+ php,
513
+ await getSqliteIntegrationZip(),
514
+ DOCROOT
515
+ );
480
516
  }
481
- };
482
- const phpInstances = [];
483
- for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) {
484
- phpInstances.push(
485
- await NodePHP.load(options.phpVersion, nodePHPOptions)
517
+ defineSiteConstants(
518
+ php,
519
+ options.absoluteUrl,
520
+ `${DOCROOT}/wp-content/database`
486
521
  );
487
522
  }
488
- const php = phpInstances[0];
489
- phpInstances.forEach((_php) => {
490
- _php.mkdirTree(documentRoot);
491
- _php.chdir(documentRoot);
492
- _php.writeFile(
493
- `${documentRoot}/index.php`,
494
- `<?php echo 'Hello wp-app!';`
495
- );
496
- });
497
- output?.log(`directory: ${options.projectPath}`);
498
- output?.log(`mode: ${options.mode}`);
499
- output?.log(`php: ${options.phpVersion}`);
500
- if (options.mode === "index" /* INDEX */) {
501
- await applyToInstances(phpInstances, async (_php) => {
502
- runIndexMode(_php, options);
503
- });
504
- return { php, phpInstances, options };
523
+ if (freshSite) {
524
+ await runInstaller(requestHandler);
505
525
  }
506
- output?.log(`wp: ${options.wordPressVersion}`);
507
- await Promise.all([
508
- downloadWordPress(options.wordPressVersion),
509
- downloadSqliteIntegrationPlugin(),
510
- downloadMuPlugins()
511
- ]);
512
- const isFirstTimeProject = !fs9.existsSync(options.wpContentPath);
513
- await applyToInstances(phpInstances, async (_php) => {
514
- switch (options.mode) {
515
- case "wp-content" /* WP_CONTENT */:
516
- await runWpContentMode(_php, options);
517
- break;
518
- case "wordpress-develop" /* WORDPRESS_DEVELOP */:
519
- await runWordPressDevelopMode(_php, options);
520
- break;
521
- case "wordpress" /* WORDPRESS */:
522
- await runWordPressMode(_php, options);
523
- break;
524
- case "plugin" /* PLUGIN */:
525
- await runPluginOrThemeMode(_php, options);
526
- break;
527
- case "theme" /* THEME */:
528
- await runPluginOrThemeMode(_php, options);
529
- break;
530
- case "playground" /* PLAYGROUND */:
531
- await runWpPlaygroundMode(_php, options);
532
- break;
533
- }
534
- });
535
- await installationStep2(php);
536
- await login(php, {
537
- username: "admin",
538
- password: "password"
539
- });
540
- if (isFirstTimeProject && ["plugin" /* PLUGIN */, "theme" /* THEME */].includes(options.mode)) {
541
- await activatePluginOrTheme(php, options);
526
+ await applyProjectMounts(php, mode, projectPath);
527
+ if (freshSite) {
528
+ await activateProject(php, mode, projectPath);
542
529
  }
543
- return {
544
- php,
545
- phpInstances,
546
- options
547
- };
548
- }
549
- async function runIndexMode(php, { documentRoot, projectPath }) {
550
- php.mount(projectPath, documentRoot);
551
- }
552
- async function runWpContentMode(php, {
553
- documentRoot,
554
- wordPressVersion,
555
- wpContentPath,
556
- projectPath,
557
- absoluteUrl
558
- }) {
559
- const wordPressPath = path13.join(
560
- getWordpressVersionsPath(),
561
- wordPressVersion
562
- );
563
- php.mount(wordPressPath, documentRoot);
564
- await initWordPress(php, wordPressVersion, documentRoot, absoluteUrl);
565
- fs9.ensureDirSync(wpContentPath);
566
- php.mount(projectPath, `${documentRoot}/wp-content`);
567
- mountSqlitePlugin(php, documentRoot);
568
- mountSqliteDatabaseDirectory(php, documentRoot, wpContentPath);
569
- mountMuPlugins(php, documentRoot);
570
- }
571
- async function runWordPressDevelopMode(php, { documentRoot, projectPath, absoluteUrl }) {
572
- await runWordPressMode(php, {
573
- documentRoot,
574
- projectPath: projectPath + "/build",
575
- absoluteUrl
576
- });
530
+ return { requestHandler, php, options, freshSite };
577
531
  }
578
- async function runWordPressMode(php, { documentRoot, wpContentPath, projectPath, absoluteUrl }) {
579
- php.mount(projectPath, documentRoot);
580
- const { initializeDefaultDatabase } = await initWordPress(
581
- php,
582
- "user-provided",
583
- documentRoot,
584
- absoluteUrl
585
- );
586
- if (initializeDefaultDatabase || fs9.existsSync(path13.join(wpContentPath, "database"))) {
587
- mountSqlitePlugin(php, documentRoot);
588
- mountSqliteDatabaseDirectory(php, documentRoot, wpContentPath);
532
+ async function applyProjectMounts(php, mode, projectPath) {
533
+ const projectName = path9.basename(projectPath);
534
+ switch (mode) {
535
+ case "theme" /* THEME */:
536
+ await mountAt(
537
+ php,
538
+ projectPath,
539
+ `${DOCROOT}/wp-content/themes/${projectName}`
540
+ );
541
+ break;
542
+ case "plugin" /* PLUGIN */:
543
+ await mountAt(
544
+ php,
545
+ projectPath,
546
+ `${DOCROOT}/wp-content/plugins/${projectName}`
547
+ );
548
+ break;
549
+ case "wp-content" /* WP_CONTENT */:
550
+ for (const entry of fs10.readdirSync(projectPath)) {
551
+ if (entry === "index.php") {
552
+ continue;
553
+ }
554
+ await mountAt(
555
+ php,
556
+ path9.join(projectPath, entry),
557
+ `${DOCROOT}/wp-content/${entry}`
558
+ );
559
+ }
560
+ break;
561
+ default:
562
+ break;
589
563
  }
590
- mountMuPlugins(php, documentRoot);
591
564
  }
592
- async function runPluginOrThemeMode(php, {
593
- wordPressVersion,
594
- documentRoot,
595
- projectPath,
596
- wpContentPath,
597
- absoluteUrl,
598
- mode
599
- }) {
600
- const wordPressPath = path13.join(
601
- getWordpressVersionsPath(),
602
- wordPressVersion
603
- );
604
- php.mount(wordPressPath, documentRoot);
605
- await initWordPress(php, wordPressVersion, documentRoot, absoluteUrl);
606
- fs9.ensureDirSync(wpContentPath);
607
- fs9.copySync(
608
- path13.join(getWordpressVersionsPath(), wordPressVersion, "wp-content"),
609
- wpContentPath
610
- );
611
- php.mount(wpContentPath, `${documentRoot}/wp-content`);
612
- const pluginName = path13.basename(projectPath);
613
- const directoryName = mode === "plugin" /* PLUGIN */ ? "plugins" : "themes";
614
- php.mount(
615
- projectPath,
616
- `${documentRoot}/wp-content/${directoryName}/${pluginName}`
617
- );
618
- mountSqlitePlugin(php, documentRoot);
619
- mountMuPlugins(php, documentRoot);
620
- }
621
- async function runWpPlaygroundMode(php, { documentRoot, wordPressVersion, wpContentPath, absoluteUrl }) {
622
- const wordPressPath = path13.join(
623
- getWordpressVersionsPath(),
624
- wordPressVersion
625
- );
626
- php.mount(wordPressPath, documentRoot);
627
- await initWordPress(php, wordPressVersion, documentRoot, absoluteUrl);
628
- fs9.ensureDirSync(wpContentPath);
629
- fs9.copySync(
630
- path13.join(getWordpressVersionsPath(), wordPressVersion, "wp-content"),
631
- wpContentPath
632
- );
633
- php.mount(wpContentPath, `${documentRoot}/wp-content`);
634
- mountSqlitePlugin(php, documentRoot);
635
- mountMuPlugins(php, documentRoot);
565
+ async function mountAt(php, hostPath, vfsPath) {
566
+ if (fs10.statSync(hostPath).isDirectory()) {
567
+ php.mkdir(vfsPath);
568
+ }
569
+ await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
636
570
  }
637
- async function initWordPress(php, wordPressVersion, vfsDocumentRoot, siteUrl) {
638
- let initializeDefaultDatabase = false;
639
- if (!php.fileExists(`${vfsDocumentRoot}/wp-config.php`)) {
640
- php.writeFile(
641
- `${vfsDocumentRoot}/wp-config.php`,
642
- php.readFileAsText(`${vfsDocumentRoot}/wp-config-sample.php`)
571
+ async function activateProject(php, mode, projectPath) {
572
+ const projectName = path9.basename(projectPath);
573
+ if (mode === "theme" /* THEME */) {
574
+ await runWordPressCode(
575
+ php,
576
+ `switch_theme(${phpString(projectName)});`
577
+ );
578
+ } else if (mode === "plugin" /* PLUGIN */) {
579
+ const pluginFile = findPluginFile(projectPath);
580
+ if (pluginFile) {
581
+ await runWordPressCode(
582
+ php,
583
+ `activate_plugin(${phpString(`${projectName}/${pluginFile}`)});`
584
+ );
585
+ }
586
+ } else if (mode === "wp-content" /* WP_CONTENT */) {
587
+ await runWordPressCode(
588
+ php,
589
+ `$theme = wp_get_theme();
590
+ if (!$theme->exists()) {
591
+ $themes = wp_get_themes();
592
+ if (count($themes) > 0) {
593
+ switch_theme(array_keys($themes)[0]);
594
+ }
595
+ }`
643
596
  );
644
- initializeDefaultDatabase = true;
645
- }
646
- const wpConfigConsts = {
647
- WP_HOME: siteUrl,
648
- WP_SITEURL: siteUrl
649
- };
650
- if (wordPressVersion !== "user-defined") {
651
- wpConfigConsts["WP_AUTO_UPDATE_CORE"] = wordPressVersion === "latest";
652
597
  }
653
- await defineWpConfigConsts(php, {
654
- consts: wpConfigConsts,
655
- virtualize: true
656
- });
657
- return { initializeDefaultDatabase };
658
598
  }
659
- async function activatePluginOrTheme(php, { projectPath, mode }) {
660
- if (mode === "plugin" /* PLUGIN */) {
661
- const pluginFile = getPluginFile(projectPath);
662
- await activatePlugin(php, { pluginPath: pluginFile });
663
- } else if (mode === "theme" /* THEME */) {
664
- const themeFolderName = path13.basename(projectPath);
665
- await activateTheme(php, { themeFolderName });
599
+ async function runWordPressCode(php, code) {
600
+ try {
601
+ await runPhp(
602
+ php,
603
+ `
604
+ require ${phpString(`${DOCROOT}/wp-load.php`)};
605
+ require_once ${phpString(`${DOCROOT}/wp-admin/includes/plugin.php`)};
606
+ ${code}
607
+ `
608
+ );
609
+ } catch (error) {
610
+ output?.error(`Activation step failed: ${error.message}`);
666
611
  }
667
612
  }
668
- function mountMuPlugins(php, vfsDocumentRoot) {
669
- php.mount(
670
- path13.join(getWpNowPath(), "mu-plugins"),
671
- // VFS paths are always POSIX — path.join would break them on Windows.
672
- `${vfsDocumentRoot}/wp-content/mu-plugins`
673
- );
613
+ function phpString(value) {
614
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
674
615
  }
675
- function mountSqlitePlugin(php, vfsDocumentRoot) {
676
- const sqlitePluginPath = `${vfsDocumentRoot}/wp-content/plugins/${SQLITE_FILENAME}`;
677
- if (php.listFiles(sqlitePluginPath).length === 0) {
678
- php.mount(getSqlitePath(), sqlitePluginPath);
679
- php.mount(
680
- path13.join(getSqlitePath(), "db.copy"),
681
- `${vfsDocumentRoot}/wp-content/db.php`
682
- );
616
+ function findPluginFile(projectPath) {
617
+ const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
618
+ for (const file of fs10.readdirSync(projectPath)) {
619
+ if (!file.endsWith(".php")) {
620
+ continue;
621
+ }
622
+ const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
623
+ if (pluginNameRegex.test(content)) {
624
+ return file;
625
+ }
683
626
  }
684
- }
685
- function mountSqliteDatabaseDirectory(php, vfsDocumentRoot, wpContentPath) {
686
- fs9.ensureDirSync(path13.join(wpContentPath, "database"));
687
- php.mount(
688
- path13.join(wpContentPath, "database"),
689
- `${vfsDocumentRoot}/wp-content/database`
690
- );
627
+ return null;
691
628
  }
692
629
  function inferMode(projectPath) {
693
630
  if (isWordPressDevelopDirectory(projectPath)) {
@@ -705,136 +642,107 @@ function inferMode(projectPath) {
705
642
  }
706
643
  return "playground" /* PLAYGROUND */;
707
644
  }
708
- async function installationStep2(php) {
709
- const fields = new URLSearchParams({
710
- language: "en",
711
- prefix: "wp_",
712
- weblog_title: "My WordPress Website",
713
- user_name: "admin",
714
- admin_password: "password",
715
- admin_password2: "password",
716
- Submit: "Install WordPress",
717
- pw_weak: "1",
718
- admin_email: "admin@localhost.com"
719
- });
720
- return php.request({
721
- url: "/wp-admin/install.php?step=2",
722
- method: "POST",
723
- headers: {
724
- "content-type": "application/x-www-form-urlencoded"
725
- },
726
- body: new TextEncoder().encode(fields.toString())
727
- });
728
- }
729
645
 
730
646
  // src/start-server.ts
731
- function requestBodyToMultipartFormData(json, boundary) {
732
- let multipartData = "";
733
- const eol = "\r\n";
734
- for (const key in json) {
735
- multipartData += `--${boundary}${eol}`;
736
- multipartData += `Content-Disposition: form-data; name="${key}"${eol}${eol}`;
737
- multipartData += `${json[key]}${eol}`;
738
- }
739
- multipartData += `--${boundary}--${eol}`;
740
- return multipartData;
741
- }
742
- var requestBodyToString = async (req) => await new Promise((resolve) => {
743
- let body = "";
744
- req.on("data", (chunk) => {
745
- body += chunk.toString();
746
- });
747
- req.on("end", () => {
748
- resolve(body);
749
- });
750
- });
751
647
  async function startServer(options = {}) {
752
- if (!fs10.existsSync(options.projectPath)) {
648
+ if (!fs11.existsSync(options.projectPath)) {
753
649
  throw new Error(
754
650
  `The given path "${options.projectPath}" does not exist.`
755
651
  );
756
652
  }
757
- const app = express();
758
- app.use(fileUpload());
759
653
  const port = await portFinder.getOpenPort();
760
- const { php, options: wpNowOptions } = await startWPNow(options);
654
+ const site = await startWPApp(options);
655
+ const app = express();
761
656
  app.use("/", async (req, res) => {
762
657
  try {
763
- const requestHeaders = {};
764
- if (req.rawHeaders && req.rawHeaders.length) {
765
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
766
- requestHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1];
767
- }
768
- }
769
- const body = requestHeaders["content-type"]?.startsWith(
770
- "multipart/form-data"
771
- ) ? requestBodyToMultipartFormData(
772
- req.body,
773
- requestHeaders["content-type"].split("; boundary=")[1]
774
- ) : await requestBodyToString(req);
775
- const data = {
658
+ const phpRequest = {
776
659
  url: req.url,
777
- headers: requestHeaders,
778
660
  method: req.method,
779
- files: Object.fromEntries(
780
- Object.entries(req.files || {}).map(
781
- ([key, file]) => [
782
- key,
783
- {
784
- key,
785
- name: file.name,
786
- size: file.size,
787
- type: file.mimetype,
788
- arrayBuffer: () => file.data.buffer
789
- }
790
- ]
791
- )
792
- ),
793
- body
661
+ headers: parseHeaders(req),
662
+ body: await bufferRequestBody(req)
794
663
  };
795
- const resp = await php.request(data);
796
- res.statusCode = resp.httpStatusCode;
797
- Object.keys(resp.headers).forEach((key) => {
798
- res.setHeader(key, resp.headers[key]);
799
- });
800
- res.end(resp.bytes);
801
- } catch (e) {
802
- output?.trace(e);
664
+ const response = await site.requestHandler.requestStreamed(phpRequest);
665
+ await sendStreamedResponse(response, res);
666
+ } catch (error) {
667
+ output?.trace(error);
668
+ if (!res.headersSent) {
669
+ res.statusCode = 500;
670
+ res.end("Internal Server Error");
671
+ }
803
672
  }
804
673
  });
805
674
  const url = options.absoluteUrl;
806
675
  app.listen(port, () => {
807
676
  output?.log(`Server running at ${url}`);
808
677
  });
809
- return {
810
- url,
811
- php,
812
- options: wpNowOptions
813
- };
678
+ return { ...site, url };
814
679
  }
680
+ async function sendStreamedResponse(streamedResponse, res) {
681
+ const [headers, httpStatusCode] = await Promise.all([
682
+ streamedResponse.headers,
683
+ streamedResponse.httpStatusCode
684
+ ]);
685
+ res.statusCode = httpStatusCode;
686
+ for (const key in headers) {
687
+ res.setHeader(key, headers[key]);
688
+ }
689
+ const nodeStream = Readable.fromWeb(streamedResponse.stdout);
690
+ try {
691
+ await pipeline(nodeStream, res);
692
+ } catch (error) {
693
+ if (error instanceof Error && "code" in error && (error.code === "ERR_STREAM_PREMATURE_CLOSE" || error.code === "ERR_STREAM_UNABLE_TO_PIPE")) {
694
+ return;
695
+ }
696
+ throw error;
697
+ }
698
+ }
699
+ var bufferRequestBody = async (req) => await new Promise((resolve) => {
700
+ const body = [];
701
+ req.on("data", (chunk) => {
702
+ body.push(chunk);
703
+ });
704
+ req.on("end", () => {
705
+ resolve(new Uint8Array(Buffer.concat(body)));
706
+ });
707
+ });
708
+ var parseHeaders = (req) => {
709
+ const requestHeaders = {};
710
+ if (req.rawHeaders && req.rawHeaders.length) {
711
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
712
+ requestHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1];
713
+ }
714
+ }
715
+ return requestHeaders;
716
+ };
815
717
 
816
718
  // src/execute-php.ts
817
- async function executePHP(phpArgs, options = {}) {
818
- if (phpArgs[0] !== "php") {
719
+ import fs12 from "fs";
720
+ import path10 from "path";
721
+ import { loadNodeRuntime as loadNodeRuntime2 } from "@php-wasm/node";
722
+ import { PHP as PHP2 } from "@php-wasm/universal";
723
+ async function executePHP(phpArgs, options) {
724
+ const args = phpArgs.filter((arg) => arg !== "php" && arg !== "--");
725
+ let code;
726
+ if (args[0] === "-r" && typeof args[1] === "string") {
727
+ code = `<?php ${args[1]}`;
728
+ } else if (args[0] && fs12.existsSync(path10.resolve(args[0]))) {
729
+ code = fs12.readFileSync(path10.resolve(args[0]), "utf8");
730
+ } else {
819
731
  throw new Error(
820
- 'The first argument to executePHP must be the string "php".'
732
+ 'Usage: wp-app php -- -r "<code>" | wp-app php -- <script.php>'
821
733
  );
822
734
  }
823
- disableOutput();
824
- const { phpInstances } = await startWPNow({
825
- ...options,
826
- numberOfPhpInstances: 2
827
- });
828
- const [, php] = phpInstances;
829
- try {
830
- php.useHostFilesystem();
831
- await php.cli(phpArgs);
832
- } catch (resultOrError) {
833
- const success = resultOrError.name === "ExitStatus" && resultOrError.status === 0;
834
- if (!success) {
835
- throw resultOrError;
836
- }
735
+ const php = new PHP2(
736
+ await loadNodeRuntime2(options.phpVersion, {
737
+ emscriptenOptions: { processId: process.pid }
738
+ })
739
+ );
740
+ const response = await php.run({ code });
741
+ process.stdout.write(response.bytes);
742
+ if (response.errors) {
743
+ process.stderr.write(response.errors);
837
744
  }
745
+ process.exitCode = response.exitCode;
838
746
  }
839
747
 
840
748
  // src/run-cli.ts
@@ -900,7 +808,7 @@ async function runCli() {
900
808
  config.mode = argv.mode;
901
809
  }
902
810
  try {
903
- await getWpNowConfig(config);
811
+ await getWpAppConfig(config);
904
812
  } catch (error) {
905
813
  return error.message;
906
814
  }
@@ -911,7 +819,7 @@ async function runCli() {
911
819
  (yargs2) => {
912
820
  serverParameters(yargs2);
913
821
  yargs2.option("reset", {
914
- describe: "Create a new project environment, destroying the old one (wp-content and database).",
822
+ describe: "Create a new site environment, destroying the old one (WordPress files and database).",
915
823
  type: "boolean",
916
824
  default: false
917
825
  });
@@ -924,7 +832,7 @@ async function runCli() {
924
832
  async (argv) => {
925
833
  const spinner = startSpinner("Starting the server...");
926
834
  try {
927
- const options = await getWpNowConfig({
835
+ const options = await getWpAppConfig({
928
836
  path: argv.path,
929
837
  php: argv.php,
930
838
  wp: argv.wp,
@@ -933,8 +841,21 @@ async function runCli() {
933
841
  });
934
842
  portFinder.setPort(options.port);
935
843
  if (argv.reset) {
936
- fs11.removeSync(options.wpContentPath);
937
- output?.log("Project environment reset.");
844
+ if (["wordpress", "wordpress-develop", "index"].includes(
845
+ options.mode
846
+ )) {
847
+ output?.log(
848
+ "--reset only applies to wp-app managed sites; your project files are never touched."
849
+ );
850
+ } else {
851
+ fs13.rmSync(getSitePath(options.projectPath), {
852
+ recursive: true,
853
+ force: true,
854
+ maxRetries: 10,
855
+ retryDelay: 100
856
+ });
857
+ output?.log("Site environment reset.");
858
+ }
938
859
  }
939
860
  const { url } = await startServer(options);
940
861
  if (argv.open) {
@@ -957,7 +878,7 @@ async function runCli() {
957
878
  async (argv) => {
958
879
  try {
959
880
  const args = process.argv.slice(2);
960
- const options = await getWpNowConfig({
881
+ const options = await getWpAppConfig({
961
882
  path: argv.path,
962
883
  php: argv.php
963
884
  });
@@ -976,9 +897,9 @@ async function runCli() {
976
897
  commonParameters(yargs2);
977
898
  },
978
899
  async (argv) => {
979
- const projectPath = argv.path ? path14.resolve(argv.path) : process.cwd();
900
+ const projectPath = argv.path ? path11.resolve(argv.path) : process.cwd();
980
901
  console.log(`Building project at ${projectPath}`);
981
- if (fs11.existsSync(path14.join(projectPath, "composer.json"))) {
902
+ if (fs13.existsSync(path11.join(projectPath, "composer.json"))) {
982
903
  console.log("Running composer install...");
983
904
  try {
984
905
  execSync(
@@ -991,9 +912,11 @@ async function runCli() {
991
912
  );
992
913
  }
993
914
  }
994
- const packageJsonPath = path14.join(projectPath, "package.json");
995
- if (fs11.existsSync(packageJsonPath)) {
996
- const pkg = fs11.readJsonSync(packageJsonPath);
915
+ const packageJsonPath = path11.join(projectPath, "package.json");
916
+ if (fs13.existsSync(packageJsonPath)) {
917
+ const pkg = JSON.parse(
918
+ fs13.readFileSync(packageJsonPath, "utf8")
919
+ );
997
920
  if (pkg.scripts?.build) {
998
921
  console.log("Running npm build...");
999
922
  execSync("npm run build", {
@@ -1012,7 +935,7 @@ async function runCli() {
1012
935
  },
1013
936
  async (argv) => {
1014
937
  const selfScript = process.argv[1];
1015
- const watchDir = path14.resolve(
938
+ const watchDir = path11.resolve(
1016
939
  argv.path || process.cwd()
1017
940
  );
1018
941
  const IGNORED = ["node_modules", "vendor", ".git", "dist"];
@@ -1033,7 +956,7 @@ async function runCli() {
1033
956
  });
1034
957
  };
1035
958
  startChild();
1036
- fs11.watch(
959
+ fs13.watch(
1037
960
  watchDir,
1038
961
  { recursive: true },
1039
962
  (_eventType, filename) => {
@@ -1086,11 +1009,11 @@ function openInDefaultBrowser(url) {
1086
1009
  }
1087
1010
 
1088
1011
  // src/main.ts
1089
- var requiredMajorVersion = 18;
1012
+ var requiredMajorVersion = 20;
1090
1013
  var currentNodeVersion = parseInt(process.versions?.node?.split(".")?.[0]);
1091
1014
  if (currentNodeVersion < requiredMajorVersion) {
1092
1015
  console.warn(
1093
- `You are running Node.js version ${currentNodeVersion}, but this application recommends at least Node.js ${requiredMajorVersion} and isn't guaranteed to work on lower versions. Please upgrade your Node.js version.`
1016
+ `You are running Node.js version ${currentNodeVersion}, but this application requires at least Node.js ${requiredMajorVersion}. Please upgrade your Node.js version.`
1094
1017
  );
1095
1018
  }
1096
1019
  runCli();