@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.
- package/README.md +24 -14
- package/dist/index.js +407 -496
- package/dist/main.js +504 -581
- package/package.json +5 -10
package/dist/main.js
CHANGED
|
@@ -1,95 +1,193 @@
|
|
|
1
1
|
// src/run-cli.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
|
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/
|
|
14
|
-
import
|
|
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
|
|
18
|
-
var
|
|
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.
|
|
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
|
|
80
|
-
import
|
|
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/
|
|
86
|
-
import fs from "fs
|
|
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/
|
|
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/
|
|
103
|
-
import fs3 from "fs
|
|
200
|
+
// src/detect/get-plugin-file.ts
|
|
201
|
+
import fs3 from "fs";
|
|
104
202
|
import path2, { basename } from "path";
|
|
105
203
|
|
|
106
|
-
// src/
|
|
107
|
-
import fs2 from "fs
|
|
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/
|
|
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/
|
|
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/
|
|
148
|
-
import fs4 from "fs
|
|
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/
|
|
161
|
-
import fs5 from "fs
|
|
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/
|
|
174
|
-
import fs6 from "fs
|
|
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/
|
|
181
|
-
import fs7 from "fs
|
|
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/
|
|
209
|
-
import
|
|
210
|
-
import
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
return path7.join(
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
347
|
+
`Failed to download ${url} (HTTP ${response.status}).`
|
|
279
348
|
);
|
|
280
349
|
}
|
|
281
|
-
await response.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
342
|
-
return
|
|
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
|
|
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/
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
await NodePHP.load(options.phpVersion, nodePHPOptions)
|
|
517
|
+
defineSiteConstants(
|
|
518
|
+
php,
|
|
519
|
+
options.absoluteUrl,
|
|
520
|
+
`${DOCROOT}/wp-content/database`
|
|
486
521
|
);
|
|
487
522
|
}
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
669
|
-
|
|
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
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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 (!
|
|
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
|
|
654
|
+
const site = await startWPApp(options);
|
|
655
|
+
const app = express();
|
|
761
656
|
app.use("/", async (req, res) => {
|
|
762
657
|
try {
|
|
763
|
-
const
|
|
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
|
-
|
|
780
|
-
|
|
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
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
-
'
|
|
732
|
+
'Usage: wp-app php -- -r "<code>" | wp-app php -- <script.php>'
|
|
821
733
|
);
|
|
822
734
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
937
|
-
|
|
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
|
|
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 ?
|
|
900
|
+
const projectPath = argv.path ? path11.resolve(argv.path) : process.cwd();
|
|
980
901
|
console.log(`Building project at ${projectPath}`);
|
|
981
|
-
if (
|
|
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 =
|
|
995
|
-
if (
|
|
996
|
-
const pkg =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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();
|