@wp-operations/wp-app 0.1.1 → 0.3.0

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