@wp-operations/wp-app 0.1.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.
package/dist/main.js ADDED
@@ -0,0 +1,1096 @@
1
+ // src/run-cli.ts
2
+ import fs11 from "fs-extra";
3
+ import path14 from "path";
4
+ import yargs from "yargs";
5
+ import { hideBin } from "yargs/helpers";
6
+ import { spawn, execSync } from "child_process";
7
+
8
+ // src/start-server.ts
9
+ import fs10 from "fs";
10
+ import express from "express";
11
+ import fileUpload from "express-fileupload";
12
+
13
+ // src/port-finder.ts
14
+ import http from "http";
15
+
16
+ // src/constants.ts
17
+ var WP_NOW_HIDDEN_FOLDER = ".wp-app";
18
+ var SQLITE_FILENAME = "sqlite-database-integration";
19
+ var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
20
+ var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
21
+ var DEFAULT_PORT = 8881;
22
+ var DEFAULT_PHP_VERSION = "8.0";
23
+ 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";
77
+
78
+ // 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";
84
+
85
+ // src/wp-playground-wordpress/has-index-file.ts
86
+ import fs from "fs-extra";
87
+ import path from "path";
88
+ function hasIndexFile(projectPath) {
89
+ return fs.existsSync(path.join(projectPath, "index.php"));
90
+ }
91
+
92
+ // src/wp-playground-wordpress/is-valid-wordpress-version.ts
93
+ function isValidWordPressVersion(version) {
94
+ const versionPattern = /^latest$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
95
+ const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?)?$/;
96
+ return versionPattern.test(version) || classicPressPattern.test(version);
97
+ }
98
+ function isClassicPressVersion(version) {
99
+ return version === "classicpress" || version.startsWith("classicpress-");
100
+ }
101
+
102
+ // src/wp-playground-wordpress/get-plugin-file.ts
103
+ import fs3 from "fs-extra";
104
+ import path2, { basename } from "path";
105
+
106
+ // src/wp-playground-wordpress/read-file-head.ts
107
+ import fs2 from "fs-extra";
108
+ function readFileHead(filePath, length = 8192) {
109
+ const buffer = Buffer.alloc(length);
110
+ const fd = fs2.openSync(filePath, "r");
111
+ fs2.readSync(fd, buffer, 0, buffer.length, 0);
112
+ const fileContentBuffer = buffer.toString("utf8");
113
+ fs2.closeSync(fd);
114
+ return fileContentBuffer.toString();
115
+ }
116
+
117
+ // src/wp-playground-wordpress/get-plugin-file.ts
118
+ function heuristicSort(files, projectPath) {
119
+ const heuristicsBestGuess = `${basename(projectPath)}.php`;
120
+ const heuristicsBestGuessIndex = files.indexOf(heuristicsBestGuess);
121
+ if (heuristicsBestGuessIndex !== -1) {
122
+ files.splice(heuristicsBestGuessIndex, 1);
123
+ files.unshift(heuristicsBestGuess);
124
+ }
125
+ return files;
126
+ }
127
+ function getPluginFile(projectPath) {
128
+ const files = heuristicSort(fs3.readdirSync(projectPath), projectPath);
129
+ for (const file of files) {
130
+ if (file.endsWith(".php")) {
131
+ const fileContent = readFileHead(path2.join(projectPath, file));
132
+ const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
133
+ if (pluginNameRegex.test(fileContent)) {
134
+ return path2.join(path2.basename(projectPath), file);
135
+ }
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ // src/wp-playground-wordpress/is-plugin-directory.ts
142
+ function isPluginDirectory(projectPath) {
143
+ const pluginFile = getPluginFile(projectPath);
144
+ return pluginFile !== null;
145
+ }
146
+
147
+ // src/wp-playground-wordpress/is-theme-directory.ts
148
+ import fs4 from "fs-extra";
149
+ import path3 from "path";
150
+ function isThemeDirectory(projectPath) {
151
+ const styleCSSExists = fs4.existsSync(path3.join(projectPath, "style.css"));
152
+ if (!styleCSSExists) {
153
+ return false;
154
+ }
155
+ const styleCSS = readFileHead(path3.join(projectPath, "style.css"));
156
+ const themeNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Theme Name:(.*)$/im;
157
+ return themeNameRegex.test(styleCSS);
158
+ }
159
+
160
+ // src/wp-playground-wordpress/is-wp-content-directory.ts
161
+ import fs5 from "fs-extra";
162
+ import path4 from "path";
163
+ function isWpContentDirectory(projectPath) {
164
+ const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
165
+ const pluginsExists = fs5.existsSync(path4.join(projectPath, "plugins"));
166
+ const themesExists = fs5.existsSync(path4.join(projectPath, "themes"));
167
+ if (muPluginsExists || pluginsExists || themesExists) {
168
+ return true;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ // src/wp-playground-wordpress/is-wordpress-directory.ts
174
+ import fs6 from "fs-extra";
175
+ import path5 from "path";
176
+ function isWordPressDirectory(projectPath) {
177
+ return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
178
+ }
179
+
180
+ // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
181
+ import fs7 from "fs-extra";
182
+ import path6 from "path";
183
+ function isWordPressDevelopDirectory(projectPath) {
184
+ const requiredFiles = [
185
+ "src",
186
+ "src/wp-content",
187
+ "src/wp-includes",
188
+ "src/wp-load.php",
189
+ "build",
190
+ "build/wp-content",
191
+ "build/wp-includes",
192
+ "build/wp-load.php"
193
+ ];
194
+ return requiredFiles.every(
195
+ (file) => fs7.existsSync(path6.join(projectPath, file))
196
+ );
197
+ }
198
+
199
+ // src/output.ts
200
+ function shouldOutput() {
201
+ return process.env.NODE_ENV !== "test";
202
+ }
203
+ var output = shouldOutput() ? console : null;
204
+ function disableOutput() {
205
+ output = null;
206
+ }
207
+
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";
214
+ import os from "os";
215
+ function getWpNowTmpPath() {
216
+ const tmpDirectory = os.tmpdir();
217
+ return path7.join(tmpDirectory, `wp-now-tests-hidden-folder`);
218
+ }
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();
226
+ }
227
+
228
+ // src/get-wordpress-versions-path.ts
229
+ import path9 from "path";
230
+ function getWordpressVersionsPath() {
231
+ return path9.join(getWpNowPath(), "wordpress-versions");
232
+ }
233
+
234
+ // src/get-sqlite-path.ts
235
+ import path10 from "path";
236
+ function getSqlitePath() {
237
+ return path10.join(getWpNowPath(), `${SQLITE_FILENAME}-main`);
238
+ }
239
+
240
+ // src/download.ts
241
+ function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) {
242
+ if (!isValidWordPressVersion(version)) {
243
+ throw new Error(
244
+ '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".'
245
+ );
246
+ }
247
+ return `https://wordpress.org/wordpress-${version}.zip`;
248
+ }
249
+ function getClassicPressVersionUrl(version) {
250
+ if (version === "classicpress" || version === "classicpress-latest") {
251
+ return CLASSICPRESS_LATEST_URL;
252
+ }
253
+ const tag = version.slice("classicpress-".length);
254
+ return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
255
+ }
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) {
277
+ throw new Error(
278
+ `Failed to download file (Status code ${response.statusCode}).`
279
+ );
280
+ }
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);
316
+ }
317
+ }
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);
339
+ }
340
+ }
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
+ );
364
+ }
365
+
366
+ // src/wp-now.ts
367
+ import {
368
+ activatePlugin,
369
+ activateTheme,
370
+ defineWpConfigConsts,
371
+ login
372
+ } from "@wp-playground/blueprints";
373
+
374
+ // src/config.ts
375
+ import {
376
+ SupportedPHPVersionsList
377
+ } from "@php-wasm/universal";
378
+ import crypto from "crypto";
379
+
380
+ // src/github-codespaces.ts
381
+ var isGitHubCodespace = Boolean(
382
+ process.env.CODESPACE_NAME && process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
383
+ );
384
+ function getCodeSpaceURL(port) {
385
+ return `https://${process.env.CODESPACE_NAME}-${port}.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
386
+ }
387
+
388
+ // src/config.ts
389
+ import path12 from "path";
390
+ var DEFAULT_OPTIONS = {
391
+ phpVersion: DEFAULT_PHP_VERSION,
392
+ wordPressVersion: DEFAULT_WORDPRESS_VERSION,
393
+ documentRoot: "/var/www/html",
394
+ projectPath: process.cwd(),
395
+ mode: "auto" /* AUTO */,
396
+ numberOfPhpInstances: 1
397
+ };
398
+ async function getAbsoluteURL() {
399
+ const port = await portFinder.getOpenPort();
400
+ if (isGitHubCodespace) {
401
+ return getCodeSpaceURL(port);
402
+ }
403
+ return `http://localhost:${port}`;
404
+ }
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) {
412
+ if (args.port) {
413
+ portFinder.setPort(args.port);
414
+ }
415
+ const port = await portFinder.getOpenPort();
416
+ const optionsFromCli = {
417
+ phpVersion: args.php,
418
+ projectPath: args.path,
419
+ wordPressVersion: args.wp,
420
+ mode: args.mode,
421
+ port
422
+ };
423
+ const options = {};
424
+ [optionsFromCli, DEFAULT_OPTIONS].forEach((config) => {
425
+ for (const key in config) {
426
+ if (!options[key]) {
427
+ options[key] = config[key];
428
+ }
429
+ }
430
+ });
431
+ if (!options.mode || options.mode === "auto") {
432
+ options.mode = inferMode(options.projectPath);
433
+ }
434
+ if (!options.wpContentPath) {
435
+ options.wpContentPath = getWpContentHomePath(
436
+ options.projectPath,
437
+ options.mode
438
+ );
439
+ }
440
+ if (!options.absoluteUrl) {
441
+ options.absoluteUrl = await getAbsoluteURL();
442
+ }
443
+ if (!isValidWordPressVersion(options.wordPressVersion)) {
444
+ throw new Error(
445
+ '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"'
446
+ );
447
+ }
448
+ if (options.phpVersion && !SupportedPHPVersionsList.includes(options.phpVersion)) {
449
+ throw new Error(
450
+ `Unsupported PHP version: ${options.phpVersion}. Supported versions: ${SupportedPHPVersionsList.join(", ")}`
451
+ );
452
+ }
453
+ return options;
454
+ }
455
+
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
+ });
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 };
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"
539
+ });
540
+ if (isFirstTimeProject && ["plugin" /* PLUGIN */, "theme" /* THEME */].includes(options.mode)) {
541
+ await activatePluginOrTheme(php, options);
542
+ }
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
+ });
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);
589
+ }
590
+ mountMuPlugins(php, documentRoot);
591
+ }
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`)
643
+ );
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
+ }
653
+ await defineWpConfigConsts(php, {
654
+ consts: wpConfigConsts,
655
+ virtualize: true
656
+ });
657
+ return { initializeDefaultDatabase };
658
+ }
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 });
666
+ }
667
+ }
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
+ );
674
+ }
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
+ );
683
+ }
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
+ );
691
+ }
692
+ function inferMode(projectPath) {
693
+ if (isWordPressDevelopDirectory(projectPath)) {
694
+ return "wordpress-develop" /* WORDPRESS_DEVELOP */;
695
+ } else if (isWordPressDirectory(projectPath)) {
696
+ return "wordpress" /* WORDPRESS */;
697
+ } else if (isWpContentDirectory(projectPath)) {
698
+ return "wp-content" /* WP_CONTENT */;
699
+ } else if (isPluginDirectory(projectPath)) {
700
+ return "plugin" /* PLUGIN */;
701
+ } else if (isThemeDirectory(projectPath)) {
702
+ return "theme" /* THEME */;
703
+ } else if (hasIndexFile(projectPath)) {
704
+ return "index" /* INDEX */;
705
+ }
706
+ return "playground" /* PLAYGROUND */;
707
+ }
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: fields.toString()
727
+ });
728
+ }
729
+
730
+ // 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
+ async function startServer(options = {}) {
752
+ if (!fs10.existsSync(options.projectPath)) {
753
+ throw new Error(
754
+ `The given path "${options.projectPath}" does not exist.`
755
+ );
756
+ }
757
+ const app = express();
758
+ app.use(fileUpload());
759
+ const port = await portFinder.getOpenPort();
760
+ const { php, options: wpNowOptions } = await startWPNow(options);
761
+ app.use("/", async (req, res) => {
762
+ 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 = {
776
+ url: req.url,
777
+ headers: requestHeaders,
778
+ 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
794
+ };
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);
803
+ }
804
+ });
805
+ const url = options.absoluteUrl;
806
+ app.listen(port, () => {
807
+ output?.log(`Server running at ${url}`);
808
+ });
809
+ return {
810
+ url,
811
+ php,
812
+ options: wpNowOptions
813
+ };
814
+ }
815
+
816
+ // src/execute-php.ts
817
+ async function executePHP(phpArgs, options = {}) {
818
+ if (phpArgs[0] !== "php") {
819
+ throw new Error(
820
+ 'The first argument to executePHP must be the string "php".'
821
+ );
822
+ }
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
+ }
837
+ }
838
+ }
839
+
840
+ // src/run-cli.ts
841
+ var MODE_CHOICES = [
842
+ "auto",
843
+ "plugin",
844
+ "theme",
845
+ "wordpress",
846
+ "wordpress-develop",
847
+ "wp-content",
848
+ "index",
849
+ "playground"
850
+ ];
851
+ function startSpinner(message) {
852
+ process.stdout.write(`${message}...
853
+ `);
854
+ return {
855
+ succeed: (text) => {
856
+ output?.log(`${text}`);
857
+ },
858
+ fail: (text) => {
859
+ output?.error(`${text}`);
860
+ }
861
+ };
862
+ }
863
+ function commonParameters(yargs2) {
864
+ return yargs2.option("path", {
865
+ describe: "Path to the PHP or WordPress project. Defaults to the current working directory.",
866
+ type: "string"
867
+ }).option("php", {
868
+ describe: "PHP version to use.",
869
+ type: "string"
870
+ });
871
+ }
872
+ function serverParameters(yargs2) {
873
+ commonParameters(yargs2);
874
+ yargs2.option("wp", {
875
+ describe: "WordPress version to use, e.g. '--wp=6.4'. Use '--wp=classicpress' for ClassicPress.",
876
+ type: "string"
877
+ });
878
+ yargs2.option("port", {
879
+ describe: "Server port",
880
+ type: "number"
881
+ });
882
+ yargs2.option("mode", {
883
+ describe: "Project mode. Defaults to auto-detection from the project directory.",
884
+ type: "string",
885
+ choices: MODE_CHOICES
886
+ });
887
+ }
888
+ async function runCli() {
889
+ return yargs(hideBin(process.argv)).scriptName("wp-app").usage("$0 <cmd> [args]").check(async (argv) => {
890
+ if (["build", "dev"].includes(argv._[0])) {
891
+ return true;
892
+ }
893
+ const config = {
894
+ php: argv.php,
895
+ path: argv.path
896
+ };
897
+ if (argv._[0] !== "php") {
898
+ config.wp = argv.wp;
899
+ config.port = argv.port;
900
+ config.mode = argv.mode;
901
+ }
902
+ try {
903
+ await getWpNowConfig(config);
904
+ } catch (error) {
905
+ return error.message;
906
+ }
907
+ return true;
908
+ }).command(
909
+ "start",
910
+ "Start the server",
911
+ (yargs2) => {
912
+ serverParameters(yargs2);
913
+ yargs2.option("reset", {
914
+ describe: "Create a new project environment, destroying the old one (wp-content and database).",
915
+ type: "boolean",
916
+ default: false
917
+ });
918
+ yargs2.option("open", {
919
+ describe: "Open the site in the default browser.",
920
+ type: "boolean",
921
+ default: true
922
+ });
923
+ },
924
+ async (argv) => {
925
+ const spinner = startSpinner("Starting the server...");
926
+ try {
927
+ const options = await getWpNowConfig({
928
+ path: argv.path,
929
+ php: argv.php,
930
+ wp: argv.wp,
931
+ port: argv.port,
932
+ mode: argv.mode
933
+ });
934
+ portFinder.setPort(options.port);
935
+ if (argv.reset) {
936
+ fs11.removeSync(options.wpContentPath);
937
+ output?.log("Project environment reset.");
938
+ }
939
+ const { url } = await startServer(options);
940
+ if (argv.open) {
941
+ openInDefaultBrowser(url);
942
+ }
943
+ } catch (error) {
944
+ output?.error(error);
945
+ spinner.fail(
946
+ `Failed to start the server: ${error.message}`
947
+ );
948
+ }
949
+ }
950
+ ).command(
951
+ "php [..args]",
952
+ "Run the php command passing the arguments to php cli",
953
+ (yargs2) => {
954
+ commonParameters(yargs2);
955
+ yargs2.strict(false);
956
+ },
957
+ async (argv) => {
958
+ try {
959
+ const args = process.argv.slice(2);
960
+ const options = await getWpNowConfig({
961
+ path: argv.path,
962
+ php: argv.php
963
+ });
964
+ const phpArgs = args.includes("--") ? argv._ : args;
965
+ await executePHP(phpArgs, options);
966
+ process.exit(0);
967
+ } catch (error) {
968
+ console.error(error);
969
+ process.exit(error.status || -1);
970
+ }
971
+ }
972
+ ).command(
973
+ "build",
974
+ "Build the project: composer install plus the npm build script, when present",
975
+ (yargs2) => {
976
+ commonParameters(yargs2);
977
+ },
978
+ async (argv) => {
979
+ const projectPath = argv.path ? path14.resolve(argv.path) : process.cwd();
980
+ console.log(`Building project at ${projectPath}`);
981
+ if (fs11.existsSync(path14.join(projectPath, "composer.json"))) {
982
+ console.log("Running composer install...");
983
+ try {
984
+ execSync(
985
+ "composer install --no-dev --optimize-autoloader",
986
+ { cwd: projectPath, stdio: "inherit" }
987
+ );
988
+ } catch (error) {
989
+ console.error(
990
+ `composer install failed: ${error.message}`
991
+ );
992
+ }
993
+ }
994
+ const packageJsonPath = path14.join(projectPath, "package.json");
995
+ if (fs11.existsSync(packageJsonPath)) {
996
+ const pkg = fs11.readJsonSync(packageJsonPath);
997
+ if (pkg.scripts?.build) {
998
+ console.log("Running npm build...");
999
+ execSync("npm run build", {
1000
+ cwd: projectPath,
1001
+ stdio: "inherit"
1002
+ });
1003
+ }
1004
+ }
1005
+ console.log("Build complete.");
1006
+ }
1007
+ ).command(
1008
+ "dev",
1009
+ "Start the server and restart it when project files change",
1010
+ (yargs2) => {
1011
+ serverParameters(yargs2);
1012
+ },
1013
+ async (argv) => {
1014
+ const selfScript = process.argv[1];
1015
+ const watchDir = path14.resolve(
1016
+ argv.path || process.cwd()
1017
+ );
1018
+ const IGNORED = ["node_modules", "vendor", ".git", "dist"];
1019
+ let child = null;
1020
+ let restartTimer = null;
1021
+ let firstRun = true;
1022
+ const startChild = () => {
1023
+ const args = [selfScript, "start"];
1024
+ if (argv.path) args.push(`--path=${argv.path}`);
1025
+ if (argv.php) args.push(`--php=${argv.php}`);
1026
+ if (argv.wp) args.push(`--wp=${argv.wp}`);
1027
+ if (argv.port) args.push(`--port=${argv.port}`);
1028
+ if (argv.mode) args.push(`--mode=${argv.mode}`);
1029
+ if (!firstRun) args.push("--no-open");
1030
+ firstRun = false;
1031
+ child = spawn(process.execPath, args, {
1032
+ stdio: "inherit"
1033
+ });
1034
+ };
1035
+ startChild();
1036
+ fs11.watch(
1037
+ watchDir,
1038
+ { recursive: true },
1039
+ (_eventType, filename) => {
1040
+ if (!filename) return;
1041
+ const parts = filename.split(/[\\/]/);
1042
+ if (parts.some(
1043
+ (part) => IGNORED.includes(part) || part.startsWith(".")
1044
+ )) {
1045
+ return;
1046
+ }
1047
+ clearTimeout(restartTimer);
1048
+ restartTimer = setTimeout(() => {
1049
+ console.log(
1050
+ `Change detected: ${filename}. Restarting...`
1051
+ );
1052
+ child?.kill();
1053
+ startChild();
1054
+ }, 500);
1055
+ }
1056
+ );
1057
+ console.log("Dev mode: watching for file changes...");
1058
+ }
1059
+ ).demandCommand(1, "You must provide a valid command").help().alias("h", "help").strict().argv;
1060
+ }
1061
+ function openInDefaultBrowser(url) {
1062
+ if (isGitHubCodespace) {
1063
+ return;
1064
+ }
1065
+ let cmd, args;
1066
+ switch (process.platform) {
1067
+ case "darwin":
1068
+ cmd = "open";
1069
+ args = [url];
1070
+ break;
1071
+ case "linux":
1072
+ cmd = "xdg-open";
1073
+ args = [url];
1074
+ break;
1075
+ case "win32":
1076
+ cmd = "cmd";
1077
+ args = ["/c", `start ${url}`];
1078
+ break;
1079
+ default:
1080
+ output?.log(`Platform '${process.platform}' not supported`);
1081
+ return;
1082
+ }
1083
+ spawn(cmd, args).on("error", function(err) {
1084
+ console.error(err.message);
1085
+ });
1086
+ }
1087
+
1088
+ // src/main.ts
1089
+ var requiredMajorVersion = 18;
1090
+ var currentNodeVersion = parseInt(process.versions?.node?.split(".")?.[0]);
1091
+ if (currentNodeVersion < requiredMajorVersion) {
1092
+ 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.`
1094
+ );
1095
+ }
1096
+ runCli();