@wp-operations/wp-app 0.1.1 → 0.2.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 +243 -489
  3. package/dist/main.js +340 -574
  4. package/package.json +7 -10
package/dist/main.js CHANGED
@@ -1,89 +1,39 @@
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, loadNodeRuntime } from "@php-wasm/node";
18
+ import { bootWordPressAndRequestHandler } from "@wp-playground/wordpress";
15
19
 
16
20
  // src/constants.ts
17
- var WP_NOW_HIDDEN_FOLDER = ".wp-app";
18
- var SQLITE_FILENAME = "sqlite-database-integration";
21
+ import { RecommendedPHPVersion } from "@wp-playground/common";
22
+ var WP_APP_HIDDEN_FOLDER = ".wp-app";
23
+ var WP_APP_HOME_ENV = "WP_APP_HOME";
19
24
  var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
20
25
  var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
21
26
  var DEFAULT_PORT = 8881;
22
- var DEFAULT_PHP_VERSION = "8.0";
27
+ var DEFAULT_PHP_VERSION = RecommendedPHPVersion;
23
28
  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";
29
+ var DOCROOT = "/wordpress";
77
30
 
78
31
  // 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";
32
+ import fs9 from "fs";
33
+ import path8 from "path";
84
34
 
85
35
  // src/wp-playground-wordpress/has-index-file.ts
86
- import fs from "fs-extra";
36
+ import fs from "fs";
87
37
  import path from "path";
88
38
  function hasIndexFile(projectPath) {
89
39
  return fs.existsSync(path.join(projectPath, "index.php"));
@@ -100,11 +50,11 @@ function isClassicPressVersion(version) {
100
50
  }
101
51
 
102
52
  // src/wp-playground-wordpress/get-plugin-file.ts
103
- import fs3 from "fs-extra";
53
+ import fs3 from "fs";
104
54
  import path2, { basename } from "path";
105
55
 
106
56
  // src/wp-playground-wordpress/read-file-head.ts
107
- import fs2 from "fs-extra";
57
+ import fs2 from "fs";
108
58
  function readFileHead(filePath, length = 8192) {
109
59
  const buffer = Buffer.alloc(length);
110
60
  const fd = fs2.openSync(filePath, "r");
@@ -145,7 +95,7 @@ function isPluginDirectory(projectPath) {
145
95
  }
146
96
 
147
97
  // src/wp-playground-wordpress/is-theme-directory.ts
148
- import fs4 from "fs-extra";
98
+ import fs4 from "fs";
149
99
  import path3 from "path";
150
100
  function isThemeDirectory(projectPath) {
151
101
  const styleCSSExists = fs4.existsSync(path3.join(projectPath, "style.css"));
@@ -158,7 +108,7 @@ function isThemeDirectory(projectPath) {
158
108
  }
159
109
 
160
110
  // src/wp-playground-wordpress/is-wp-content-directory.ts
161
- import fs5 from "fs-extra";
111
+ import fs5 from "fs";
162
112
  import path4 from "path";
163
113
  function isWpContentDirectory(projectPath) {
164
114
  const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
@@ -171,14 +121,14 @@ function isWpContentDirectory(projectPath) {
171
121
  }
172
122
 
173
123
  // src/wp-playground-wordpress/is-wordpress-directory.ts
174
- import fs6 from "fs-extra";
124
+ import fs6 from "fs";
175
125
  import path5 from "path";
176
126
  function isWordPressDirectory(projectPath) {
177
127
  return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
178
128
  }
179
129
 
180
130
  // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
181
- import fs7 from "fs-extra";
131
+ import fs7 from "fs";
182
132
  import path6 from "path";
183
133
  function isWordPressDevelopDirectory(projectPath) {
184
134
  const requiredFiles = [
@@ -201,44 +151,30 @@ function shouldOutput() {
201
151
  return process.env.NODE_ENV !== "test";
202
152
  }
203
153
  var output = shouldOutput() ? console : null;
204
- function disableOutput() {
205
- output = null;
206
- }
207
154
 
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";
155
+ // src/paths.ts
156
+ import crypto from "crypto";
157
+ import fs8 from "fs";
214
158
  import os from "os";
215
- function getWpNowTmpPath() {
216
- const tmpDirectory = os.tmpdir();
217
- return path7.join(tmpDirectory, `wp-now-tests-hidden-folder`);
159
+ import path7 from "path";
160
+ function getWpAppHome() {
161
+ return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
218
162
  }
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();
163
+ function getCachePath() {
164
+ return ensureDir(path7.join(getWpAppHome(), "cache"));
226
165
  }
227
-
228
- // src/get-wordpress-versions-path.ts
229
- import path9 from "path";
230
- function getWordpressVersionsPath() {
231
- return path9.join(getWpNowPath(), "wordpress-versions");
166
+ function getSitePath(projectPath) {
167
+ const projectName = path7.basename(path7.resolve(projectPath));
168
+ const hash = crypto.createHash("sha1").update(path7.resolve(projectPath)).digest("hex");
169
+ return path7.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
232
170
  }
233
-
234
- // src/get-sqlite-path.ts
235
- import path10 from "path";
236
- function getSqlitePath() {
237
- return path10.join(getWpNowPath(), `${SQLITE_FILENAME}-main`);
171
+ function ensureDir(dir) {
172
+ fs8.mkdirSync(dir, { recursive: true });
173
+ return dir;
238
174
  }
239
175
 
240
176
  // src/download.ts
241
- function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) {
177
+ function getWordPressVersionUrl(version) {
242
178
  if (!isValidWordPressVersion(version)) {
243
179
  throw new Error(
244
180
  '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 +189,35 @@ function getClassicPressVersionUrl(version) {
253
189
  const tag = version.slice("classicpress-".length);
254
190
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
255
191
  }
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) {
192
+ async function cachedZip(url, cacheKey) {
193
+ const cacheFile = path8.join(getCachePath(), cacheKey);
194
+ if (!fs9.existsSync(cacheFile)) {
195
+ output?.log(`Downloading ${cacheKey}...`);
196
+ const response = await fetch(url, { redirect: "follow" });
197
+ if (!response.ok) {
277
198
  throw new Error(
278
- `Failed to download file (Status code ${response.statusCode}).`
199
+ `Failed to download ${url} (HTTP ${response.status}).`
279
200
  );
280
201
  }
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);
202
+ const bytes = Buffer.from(await response.arrayBuffer());
203
+ fs9.writeFileSync(`${cacheFile}.partial`, bytes);
204
+ fs9.renameSync(`${cacheFile}.partial`, cacheFile);
205
+ output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
316
206
  }
207
+ return new File([fs9.readFileSync(cacheFile)], cacheKey);
317
208
  }
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);
209
+ async function getCoreZip(version) {
210
+ if (isClassicPressVersion(version)) {
211
+ return cachedZip(getClassicPressVersionUrl(version), `${version}.zip`);
339
212
  }
213
+ return cachedZip(getWordPressVersionUrl(version), `wordpress-${version}.zip`);
340
214
  }
341
- async function downloadSqliteIntegrationPlugin() {
342
- return downloadFileAndUnzip({
343
- url: SQLITE_URL,
344
- destinationFolder: getWpNowPath(),
345
- checkFinalPath: getSqlitePath(),
346
- itemName: "SQLite"
347
- });
215
+ async function getSqliteIntegrationZip() {
216
+ return cachedZip(SQLITE_URL, "sqlite-database-integration.zip");
348
217
  }
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
- );
364
- }
365
-
366
- // src/wp-now.ts
367
- import {
368
- activatePlugin,
369
- activateTheme,
370
- defineWpConfigConsts,
371
- login
372
- } from "@wp-playground/blueprints";
373
218
 
374
219
  // src/config.ts
375
- import {
376
- SupportedPHPVersionsList
377
- } from "@php-wasm/universal";
378
- import crypto from "crypto";
220
+ import { SupportedPHPVersionsList } from "@php-wasm/universal";
379
221
 
380
222
  // src/github-codespaces.ts
381
223
  var isGitHubCodespace = Boolean(
@@ -385,15 +227,61 @@ function getCodeSpaceURL(port) {
385
227
  return `https://${process.env.CODESPACE_NAME}-${port}.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
386
228
  }
387
229
 
230
+ // src/port-finder.ts
231
+ import http from "http";
232
+ var PortFinder = class _PortFinder {
233
+ static #instance;
234
+ #searchPort = DEFAULT_PORT;
235
+ #openPort = null;
236
+ constructor() {
237
+ }
238
+ static getInstance() {
239
+ if (!_PortFinder.#instance) {
240
+ _PortFinder.#instance = new _PortFinder();
241
+ }
242
+ return _PortFinder.#instance;
243
+ }
244
+ #incrementPort() {
245
+ return this.#searchPort++;
246
+ }
247
+ #isPortFree() {
248
+ return new Promise((resolve) => {
249
+ const server = http.createServer();
250
+ server.listen(this.#searchPort, () => {
251
+ server.close();
252
+ resolve(true);
253
+ }).on("error", () => {
254
+ resolve(false);
255
+ });
256
+ });
257
+ }
258
+ /**
259
+ * Returns the first available open port, caching and reusing it for subsequent calls.
260
+ *
261
+ * @returns {Promise<number>} A promise that resolves to the open port number.
262
+ */
263
+ async getOpenPort() {
264
+ if (this.#openPort) {
265
+ return this.#openPort;
266
+ }
267
+ while (!await this.#isPortFree()) {
268
+ this.#incrementPort();
269
+ }
270
+ this.#openPort = this.#searchPort;
271
+ return this.#openPort;
272
+ }
273
+ setPort(port) {
274
+ this.#openPort = port;
275
+ }
276
+ };
277
+ var portFinder = PortFinder.getInstance();
278
+
388
279
  // src/config.ts
389
- import path12 from "path";
390
280
  var DEFAULT_OPTIONS = {
391
281
  phpVersion: DEFAULT_PHP_VERSION,
392
282
  wordPressVersion: DEFAULT_WORDPRESS_VERSION,
393
- documentRoot: "/var/www/html",
394
283
  projectPath: process.cwd(),
395
- mode: "auto" /* AUTO */,
396
- numberOfPhpInstances: 1
284
+ mode: "auto" /* AUTO */
397
285
  };
398
286
  async function getAbsoluteURL() {
399
287
  const port = await portFinder.getOpenPort();
@@ -402,13 +290,7 @@ async function getAbsoluteURL() {
402
290
  }
403
291
  return `http://localhost:${port}`;
404
292
  }
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) {
293
+ async function getWpAppConfig(args) {
412
294
  if (args.port) {
413
295
  portFinder.setPort(args.port);
414
296
  }
@@ -431,12 +313,6 @@ async function getWpNowConfig(args) {
431
313
  if (!options.mode || options.mode === "auto") {
432
314
  options.mode = inferMode(options.projectPath);
433
315
  }
434
- if (!options.wpContentPath) {
435
- options.wpContentPath = getWpContentHomePath(
436
- options.projectPath,
437
- options.mode
438
- );
439
- }
440
316
  if (!options.absoluteUrl) {
441
317
  options.absoluteUrl = await getAbsoluteURL();
442
318
  }
@@ -453,241 +329,145 @@ async function getWpNowConfig(args) {
453
329
  return options;
454
330
  }
455
331
 
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]);
463
- }
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
- }
480
- }
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)
486
- );
487
- }
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
- });
332
+ // src/engine.ts
333
+ async function startWPApp(options) {
334
+ const projectPath = path9.resolve(options.projectPath);
335
+ const mode = options.mode;
497
336
  output?.log(`directory: ${options.projectPath}`);
498
- output?.log(`mode: ${options.mode}`);
337
+ output?.log(`mode: ${mode}`);
499
338
  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 };
505
- }
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"
339
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
340
+ const isWordPressBacked = mode !== "index" /* INDEX */;
341
+ const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
342
+ if (isWordPressBacked) {
343
+ output?.log(`wp: ${options.wordPressVersion}`);
344
+ output?.log(`site data: ${hostDocroot}`);
345
+ }
346
+ const requestHandler = await bootWordPressAndRequestHandler({
347
+ siteUrl: options.absoluteUrl,
348
+ documentRoot: DOCROOT,
349
+ phpVersion: options.phpVersion,
350
+ maxPhpInstances: 1,
351
+ sapiName: "cli",
352
+ cookieStore: false,
353
+ createPhpRuntime: () => loadNodeRuntime(options.phpVersion, {
354
+ emscriptenOptions: { processId: process.pid }
355
+ }),
356
+ hooks: {
357
+ beforeWordPressFiles: async (php2) => {
358
+ php2.mkdir(DOCROOT);
359
+ await php2.mount(
360
+ DOCROOT,
361
+ createNodeFsMountHandler(hostDocroot)
362
+ );
363
+ }
364
+ },
365
+ wordPressZip: freshSite ? await getCoreZip(options.wordPressVersion) : void 0,
366
+ sqliteIntegrationPluginZip: isWordPressBacked ? await getSqliteIntegrationZip() : void 0,
367
+ dataSqlPath: isWordPressBacked ? `${DOCROOT}/wp-content/database/.ht.sqlite` : void 0,
368
+ wordpressInstallMode: isWordPressBacked ? "install-from-existing-files-if-needed" : "do-not-attempt-installing"
539
369
  });
540
- if (isFirstTimeProject && ["plugin" /* PLUGIN */, "theme" /* THEME */].includes(options.mode)) {
541
- await activatePluginOrTheme(php, options);
370
+ const php = await requestHandler.getPrimaryPhp();
371
+ await applyProjectMounts(php, mode, projectPath);
372
+ if (freshSite) {
373
+ await activateProject(php, mode, projectPath);
374
+ }
375
+ return { requestHandler, php, options, freshSite };
376
+ }
377
+ async function applyProjectMounts(php, mode, projectPath) {
378
+ const projectName = path9.basename(projectPath);
379
+ switch (mode) {
380
+ case "theme" /* THEME */:
381
+ await mountAt(
382
+ php,
383
+ projectPath,
384
+ `${DOCROOT}/wp-content/themes/${projectName}`
385
+ );
386
+ break;
387
+ case "plugin" /* PLUGIN */:
388
+ await mountAt(
389
+ php,
390
+ projectPath,
391
+ `${DOCROOT}/wp-content/plugins/${projectName}`
392
+ );
393
+ break;
394
+ case "wp-content" /* WP_CONTENT */:
395
+ for (const entry of fs10.readdirSync(projectPath)) {
396
+ if (entry === "index.php") {
397
+ continue;
398
+ }
399
+ await mountAt(
400
+ php,
401
+ path9.join(projectPath, entry),
402
+ `${DOCROOT}/wp-content/${entry}`
403
+ );
404
+ }
405
+ break;
406
+ default:
407
+ break;
542
408
  }
543
- return {
544
- php,
545
- phpInstances,
546
- options
547
- };
548
409
  }
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
- });
577
- }
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);
410
+ async function mountAt(php, hostPath, vfsPath) {
411
+ if (fs10.statSync(hostPath).isDirectory()) {
412
+ php.mkdir(vfsPath);
589
413
  }
590
- mountMuPlugins(php, documentRoot);
414
+ await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
591
415
  }
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);
636
- }
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`)
416
+ async function activateProject(php, mode, projectPath) {
417
+ const projectName = path9.basename(projectPath);
418
+ if (mode === "theme" /* THEME */) {
419
+ await runWordPressCode(
420
+ php,
421
+ `switch_theme(${phpString(projectName)});`
422
+ );
423
+ } else if (mode === "plugin" /* PLUGIN */) {
424
+ const pluginFile = findPluginFile(projectPath);
425
+ if (pluginFile) {
426
+ await runWordPressCode(
427
+ php,
428
+ `activate_plugin(${phpString(`${projectName}/${pluginFile}`)});`
429
+ );
430
+ }
431
+ } else if (mode === "wp-content" /* WP_CONTENT */) {
432
+ await runWordPressCode(
433
+ php,
434
+ `$theme = wp_get_theme();
435
+ if (!$theme->exists()) {
436
+ $themes = wp_get_themes();
437
+ if (count($themes) > 0) {
438
+ switch_theme(array_keys($themes)[0]);
439
+ }
440
+ }`
643
441
  );
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
442
  }
653
- await defineWpConfigConsts(php, {
654
- consts: wpConfigConsts,
655
- virtualize: true
656
- });
657
- return { initializeDefaultDatabase };
658
443
  }
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 });
444
+ async function runWordPressCode(php, code) {
445
+ const response = await php.run({
446
+ code: `<?php
447
+ require ${phpString(`${DOCROOT}/wp-load.php`)};
448
+ require_once ${phpString(`${DOCROOT}/wp-admin/includes/plugin.php`)};
449
+ ${code}
450
+ `
451
+ });
452
+ if (response.exitCode !== 0) {
453
+ output?.error(`Activation step failed: ${response.errors}`);
666
454
  }
667
455
  }
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
- );
456
+ function phpString(value) {
457
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
674
458
  }
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
- );
459
+ function findPluginFile(projectPath) {
460
+ const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
461
+ for (const file of fs10.readdirSync(projectPath)) {
462
+ if (!file.endsWith(".php")) {
463
+ continue;
464
+ }
465
+ const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
466
+ if (pluginNameRegex.test(content)) {
467
+ return file;
468
+ }
683
469
  }
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
- );
470
+ return null;
691
471
  }
692
472
  function inferMode(projectPath) {
693
473
  if (isWordPressDevelopDirectory(projectPath)) {
@@ -705,136 +485,107 @@ function inferMode(projectPath) {
705
485
  }
706
486
  return "playground" /* PLAYGROUND */;
707
487
  }
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
488
 
730
489
  // 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
490
  async function startServer(options = {}) {
752
- if (!fs10.existsSync(options.projectPath)) {
491
+ if (!fs11.existsSync(options.projectPath)) {
753
492
  throw new Error(
754
493
  `The given path "${options.projectPath}" does not exist.`
755
494
  );
756
495
  }
757
- const app = express();
758
- app.use(fileUpload());
759
496
  const port = await portFinder.getOpenPort();
760
- const { php, options: wpNowOptions } = await startWPNow(options);
497
+ const site = await startWPApp(options);
498
+ const app = express();
761
499
  app.use("/", async (req, res) => {
762
500
  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 = {
501
+ const phpRequest = {
776
502
  url: req.url,
777
- headers: requestHeaders,
778
503
  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
504
+ headers: parseHeaders(req),
505
+ body: await bufferRequestBody(req)
794
506
  };
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);
507
+ const response = await site.requestHandler.requestStreamed(phpRequest);
508
+ await sendStreamedResponse(response, res);
509
+ } catch (error) {
510
+ output?.trace(error);
511
+ if (!res.headersSent) {
512
+ res.statusCode = 500;
513
+ res.end("Internal Server Error");
514
+ }
803
515
  }
804
516
  });
805
517
  const url = options.absoluteUrl;
806
518
  app.listen(port, () => {
807
519
  output?.log(`Server running at ${url}`);
808
520
  });
809
- return {
810
- url,
811
- php,
812
- options: wpNowOptions
813
- };
521
+ return { ...site, url };
814
522
  }
523
+ async function sendStreamedResponse(streamedResponse, res) {
524
+ const [headers, httpStatusCode] = await Promise.all([
525
+ streamedResponse.headers,
526
+ streamedResponse.httpStatusCode
527
+ ]);
528
+ res.statusCode = httpStatusCode;
529
+ for (const key in headers) {
530
+ res.setHeader(key, headers[key]);
531
+ }
532
+ const nodeStream = Readable.fromWeb(streamedResponse.stdout);
533
+ try {
534
+ await pipeline(nodeStream, res);
535
+ } catch (error) {
536
+ if (error instanceof Error && "code" in error && (error.code === "ERR_STREAM_PREMATURE_CLOSE" || error.code === "ERR_STREAM_UNABLE_TO_PIPE")) {
537
+ return;
538
+ }
539
+ throw error;
540
+ }
541
+ }
542
+ var bufferRequestBody = async (req) => await new Promise((resolve) => {
543
+ const body = [];
544
+ req.on("data", (chunk) => {
545
+ body.push(chunk);
546
+ });
547
+ req.on("end", () => {
548
+ resolve(new Uint8Array(Buffer.concat(body)));
549
+ });
550
+ });
551
+ var parseHeaders = (req) => {
552
+ const requestHeaders = {};
553
+ if (req.rawHeaders && req.rawHeaders.length) {
554
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
555
+ requestHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1];
556
+ }
557
+ }
558
+ return requestHeaders;
559
+ };
815
560
 
816
561
  // src/execute-php.ts
817
- async function executePHP(phpArgs, options = {}) {
818
- if (phpArgs[0] !== "php") {
562
+ import fs12 from "fs";
563
+ import path10 from "path";
564
+ import { loadNodeRuntime as loadNodeRuntime2 } from "@php-wasm/node";
565
+ import { PHP } from "@php-wasm/universal";
566
+ async function executePHP(phpArgs, options) {
567
+ const args = phpArgs.filter((arg) => arg !== "php" && arg !== "--");
568
+ let code;
569
+ if (args[0] === "-r" && typeof args[1] === "string") {
570
+ code = `<?php ${args[1]}`;
571
+ } else if (args[0] && fs12.existsSync(path10.resolve(args[0]))) {
572
+ code = fs12.readFileSync(path10.resolve(args[0]), "utf8");
573
+ } else {
819
574
  throw new Error(
820
- 'The first argument to executePHP must be the string "php".'
575
+ 'Usage: wp-app php -- -r "<code>" | wp-app php -- <script.php>'
821
576
  );
822
577
  }
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
- }
578
+ const php = new PHP(
579
+ await loadNodeRuntime2(options.phpVersion, {
580
+ emscriptenOptions: { processId: process.pid }
581
+ })
582
+ );
583
+ const response = await php.run({ code });
584
+ process.stdout.write(response.bytes);
585
+ if (response.errors) {
586
+ process.stderr.write(response.errors);
837
587
  }
588
+ process.exitCode = response.exitCode;
838
589
  }
839
590
 
840
591
  // src/run-cli.ts
@@ -900,7 +651,7 @@ async function runCli() {
900
651
  config.mode = argv.mode;
901
652
  }
902
653
  try {
903
- await getWpNowConfig(config);
654
+ await getWpAppConfig(config);
904
655
  } catch (error) {
905
656
  return error.message;
906
657
  }
@@ -911,7 +662,7 @@ async function runCli() {
911
662
  (yargs2) => {
912
663
  serverParameters(yargs2);
913
664
  yargs2.option("reset", {
914
- describe: "Create a new project environment, destroying the old one (wp-content and database).",
665
+ describe: "Create a new site environment, destroying the old one (WordPress files and database).",
915
666
  type: "boolean",
916
667
  default: false
917
668
  });
@@ -924,7 +675,7 @@ async function runCli() {
924
675
  async (argv) => {
925
676
  const spinner = startSpinner("Starting the server...");
926
677
  try {
927
- const options = await getWpNowConfig({
678
+ const options = await getWpAppConfig({
928
679
  path: argv.path,
929
680
  php: argv.php,
930
681
  wp: argv.wp,
@@ -933,8 +684,21 @@ async function runCli() {
933
684
  });
934
685
  portFinder.setPort(options.port);
935
686
  if (argv.reset) {
936
- fs11.removeSync(options.wpContentPath);
937
- output?.log("Project environment reset.");
687
+ if (["wordpress", "wordpress-develop", "index"].includes(
688
+ options.mode
689
+ )) {
690
+ output?.log(
691
+ "--reset only applies to wp-app managed sites; your project files are never touched."
692
+ );
693
+ } else {
694
+ fs13.rmSync(getSitePath(options.projectPath), {
695
+ recursive: true,
696
+ force: true,
697
+ maxRetries: 10,
698
+ retryDelay: 100
699
+ });
700
+ output?.log("Site environment reset.");
701
+ }
938
702
  }
939
703
  const { url } = await startServer(options);
940
704
  if (argv.open) {
@@ -957,7 +721,7 @@ async function runCli() {
957
721
  async (argv) => {
958
722
  try {
959
723
  const args = process.argv.slice(2);
960
- const options = await getWpNowConfig({
724
+ const options = await getWpAppConfig({
961
725
  path: argv.path,
962
726
  php: argv.php
963
727
  });
@@ -976,9 +740,9 @@ async function runCli() {
976
740
  commonParameters(yargs2);
977
741
  },
978
742
  async (argv) => {
979
- const projectPath = argv.path ? path14.resolve(argv.path) : process.cwd();
743
+ const projectPath = argv.path ? path11.resolve(argv.path) : process.cwd();
980
744
  console.log(`Building project at ${projectPath}`);
981
- if (fs11.existsSync(path14.join(projectPath, "composer.json"))) {
745
+ if (fs13.existsSync(path11.join(projectPath, "composer.json"))) {
982
746
  console.log("Running composer install...");
983
747
  try {
984
748
  execSync(
@@ -991,9 +755,11 @@ async function runCli() {
991
755
  );
992
756
  }
993
757
  }
994
- const packageJsonPath = path14.join(projectPath, "package.json");
995
- if (fs11.existsSync(packageJsonPath)) {
996
- const pkg = fs11.readJsonSync(packageJsonPath);
758
+ const packageJsonPath = path11.join(projectPath, "package.json");
759
+ if (fs13.existsSync(packageJsonPath)) {
760
+ const pkg = JSON.parse(
761
+ fs13.readFileSync(packageJsonPath, "utf8")
762
+ );
997
763
  if (pkg.scripts?.build) {
998
764
  console.log("Running npm build...");
999
765
  execSync("npm run build", {
@@ -1012,7 +778,7 @@ async function runCli() {
1012
778
  },
1013
779
  async (argv) => {
1014
780
  const selfScript = process.argv[1];
1015
- const watchDir = path14.resolve(
781
+ const watchDir = path11.resolve(
1016
782
  argv.path || process.cwd()
1017
783
  );
1018
784
  const IGNORED = ["node_modules", "vendor", ".git", "dist"];
@@ -1033,7 +799,7 @@ async function runCli() {
1033
799
  });
1034
800
  };
1035
801
  startChild();
1036
- fs11.watch(
802
+ fs13.watch(
1037
803
  watchDir,
1038
804
  { recursive: true },
1039
805
  (_eventType, filename) => {
@@ -1086,11 +852,11 @@ function openInDefaultBrowser(url) {
1086
852
  }
1087
853
 
1088
854
  // src/main.ts
1089
- var requiredMajorVersion = 18;
855
+ var requiredMajorVersion = 20;
1090
856
  var currentNodeVersion = parseInt(process.versions?.node?.split(".")?.[0]);
1091
857
  if (currentNodeVersion < requiredMajorVersion) {
1092
858
  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.`
859
+ `You are running Node.js version ${currentNodeVersion}, but this application requires at least Node.js ${requiredMajorVersion}. Please upgrade your Node.js version.`
1094
860
  );
1095
861
  }
1096
862
  runCli();