@wp-operations/wp-app 0.2.0 → 0.4.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 +21 -34
  2. package/dist/index.js +345 -109
  3. package/dist/main.js +896 -322
  4. package/package.json +5 -8
package/dist/index.js CHANGED
@@ -10,48 +10,211 @@ function getCodeSpaceURL(port) {
10
10
  }
11
11
 
12
12
  // src/engine.ts
13
- import fs10 from "fs";
14
- import path9 from "path";
15
- import { createNodeFsMountHandler, loadNodeRuntime } from "@php-wasm/node";
16
- import { bootWordPressAndRequestHandler } from "@wp-playground/wordpress";
13
+ import fs11 from "fs";
14
+ import path10 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({
41
+ php,
42
+ documentRoot,
43
+ absoluteUrl,
44
+ // Route unmatched URLs (permalinks, admin-ajax) to index.php, not 404.
45
+ getFileNotFoundAction: () => ({
46
+ type: "internal-redirect",
47
+ uri: "/index.php"
48
+ }),
49
+ // Let Set-Cookie pass through to the real HTTP server (express).
50
+ cookieStore: false
51
+ });
52
+ }
53
+ async function runPhp(php, code) {
54
+ const response = await php.run({ code: `<?php ${code}` });
55
+ if (response.exitCode !== 0) {
56
+ throw new Error(response.errors || `PHP exited ${response.exitCode}`);
57
+ }
58
+ return response;
59
+ }
60
+ async function unzipTo(php, zip, destination) {
61
+ const tmpZip = `/internal/upload-${Date.now()}.zip`;
62
+ php.writeFile(tmpZip, new Uint8Array(await zip.arrayBuffer()));
63
+ await runPhp(
64
+ php,
65
+ `
66
+ $zip = new ZipArchive();
67
+ if ($zip->open(${str(tmpZip)}) !== true) exit(1);
68
+ if (!is_dir(${str(destination)})) mkdir(${str(destination)}, 0777, true);
69
+ if (!$zip->extractTo(${str(destination)})) exit(1);
70
+ $zip->close();
71
+ unlink(${str(tmpZip)});
72
+ `
73
+ );
74
+ }
75
+ async function installCore(php, coreZip, docroot) {
76
+ const staging = `${docroot}/.wpapp-staging`;
77
+ await unzipTo(php, coreZip, staging);
78
+ await runPhp(
79
+ php,
80
+ `
81
+ function wpapp_core_root($dir) {
82
+ if (file_exists("$dir/wp-config-sample.php")) return $dir;
83
+ foreach (scandir($dir) as $entry) {
84
+ if ($entry === '.' || $entry === '..') continue;
85
+ $candidate = "$dir/$entry";
86
+ if (is_dir($candidate) && file_exists("$candidate/wp-config-sample.php")) {
87
+ return $candidate;
88
+ }
89
+ }
90
+ exit(1);
91
+ }
92
+ function wpapp_rrmdir($dir) {
93
+ foreach (scandir($dir) as $entry) {
94
+ if ($entry === '.' || $entry === '..') continue;
95
+ $path = "$dir/$entry";
96
+ is_dir($path) ? wpapp_rrmdir($path) : unlink($path);
97
+ }
98
+ rmdir($dir);
99
+ }
100
+ $root = wpapp_core_root(${str(staging)});
101
+ foreach (scandir($root) as $entry) {
102
+ if ($entry === '.' || $entry === '..') continue;
103
+ rename("$root/$entry", ${str(docroot)} . "/$entry");
104
+ }
105
+ wpapp_rrmdir(${str(staging)});
106
+ if (!file_exists(${str(docroot)} . '/wp-config.php')) {
107
+ copy(
108
+ ${str(docroot)} . '/wp-config-sample.php',
109
+ ${str(docroot)} . '/wp-config.php'
110
+ );
111
+ }
112
+ `
113
+ );
114
+ }
115
+ async function installSqliteIntegration(php, pluginZip, docroot) {
116
+ const pluginDir = `${docroot}/wp-content/plugins/sqlite-database-integration`;
117
+ const staging = `${docroot}/wp-content/.wpapp-sqlite-staging`;
118
+ await unzipTo(php, pluginZip, staging);
119
+ await runPhp(
120
+ php,
121
+ `
122
+ $root = ${str(staging)};
123
+ $entries = array_values(array_diff(scandir($root), ['.', '..']));
124
+ if (count($entries) === 1 && is_dir("$root/$entries[0]")) {
125
+ $root = "$root/$entries[0]";
126
+ }
127
+ if (!is_dir(${str(pluginDir)})) {
128
+ rename($root, ${str(pluginDir)});
129
+ }
130
+ if (is_dir(${str(staging)})) {
131
+ @rmdir(${str(staging)});
132
+ }
133
+ $dropIn = file_get_contents(${str(pluginDir)} . '/db.copy');
134
+ $dropIn = str_replace(
135
+ '{SQLITE_IMPLEMENTATION_FOLDER_PATH}',
136
+ ${str(pluginDir)},
137
+ $dropIn
138
+ );
139
+ $dropIn = str_replace(
140
+ '{SQLITE_PLUGIN}',
141
+ 'sqlite-database-integration/load.php',
142
+ $dropIn
143
+ );
144
+ file_put_contents(${str(docroot)} . '/wp-content/db.php', $dropIn);
145
+ `
146
+ );
147
+ }
148
+ function defineSiteConstants(php, siteUrl, databaseDir) {
149
+ php.defineConstant("WP_HOME", siteUrl);
150
+ php.defineConstant("WP_SITEURL", siteUrl);
151
+ php.defineConstant("DB_DIR", databaseDir);
152
+ php.defineConstant("DB_FILE", ".ht.sqlite");
153
+ php.defineConstant("WP_SQLITE_AST_DRIVER", true);
154
+ php.defineConstant("DISABLE_WP_CRON", true);
155
+ }
156
+ async function runInstaller(requestHandler) {
157
+ const adminUser = process.env.WP_ADMIN_USER || "admin";
158
+ const adminPassword = process.env.WP_ADMIN_PASSWORD || "password";
159
+ const adminEmail = process.env.WP_ADMIN_EMAIL || "admin@localhost.com";
160
+ const fields = new URLSearchParams({
161
+ language: "en",
162
+ prefix: "wp_",
163
+ weblog_title: "My WordPress Website",
164
+ user_name: adminUser,
165
+ admin_password: adminPassword,
166
+ admin_password2: adminPassword,
167
+ Submit: "Install WordPress",
168
+ pw_weak: "1",
169
+ admin_email: adminEmail
170
+ });
171
+ return requestHandler.request({
172
+ url: "/wp-admin/install.php?step=2",
173
+ method: "POST",
174
+ headers: { "content-type": "application/x-www-form-urlencoded" },
175
+ body: new TextEncoder().encode(fields.toString())
176
+ });
177
+ }
178
+ function str(value) {
179
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
180
+ }
17
181
 
18
182
  // src/constants.ts
19
- import { RecommendedPHPVersion } from "@wp-playground/common";
20
183
  var WP_APP_HIDDEN_FOLDER = ".wp-app";
21
184
  var WP_APP_HOME_ENV = "WP_APP_HOME";
22
185
  var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
23
186
  var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
24
187
  var DEFAULT_PORT = 8881;
25
- var DEFAULT_PHP_VERSION = RecommendedPHPVersion;
188
+ var DEFAULT_PHP_VERSION = "8.3";
26
189
  var DEFAULT_WORDPRESS_VERSION = "latest";
27
190
  var DOCROOT = "/wordpress";
28
191
 
29
192
  // src/download.ts
30
- import fs9 from "fs";
31
- import path8 from "path";
193
+ import fs10 from "fs";
194
+ import path9 from "path";
32
195
 
33
- // src/wp-playground-wordpress/has-index-file.ts
196
+ // src/detect/has-index-file.ts
34
197
  import fs from "fs";
35
198
  import path from "path";
36
199
  function hasIndexFile(projectPath) {
37
200
  return fs.existsSync(path.join(projectPath, "index.php"));
38
201
  }
39
202
 
40
- // src/wp-playground-wordpress/is-valid-wordpress-version.ts
203
+ // src/detect/is-valid-wordpress-version.ts
41
204
  function isValidWordPressVersion(version) {
42
205
  const versionPattern = /^latest$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
43
- const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?)?$/;
206
+ const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?(?:-rc(?:\d+)?)?)?$/;
44
207
  return versionPattern.test(version) || classicPressPattern.test(version);
45
208
  }
46
209
  function isClassicPressVersion(version) {
47
210
  return version === "classicpress" || version.startsWith("classicpress-");
48
211
  }
49
212
 
50
- // src/wp-playground-wordpress/get-plugin-file.ts
213
+ // src/detect/get-plugin-file.ts
51
214
  import fs3 from "fs";
52
215
  import path2, { basename } from "path";
53
216
 
54
- // src/wp-playground-wordpress/read-file-head.ts
217
+ // src/detect/read-file-head.ts
55
218
  import fs2 from "fs";
56
219
  function readFileHead(filePath, length = 8192) {
57
220
  const buffer = Buffer.alloc(length);
@@ -62,7 +225,7 @@ function readFileHead(filePath, length = 8192) {
62
225
  return fileContentBuffer.toString();
63
226
  }
64
227
 
65
- // src/wp-playground-wordpress/get-plugin-file.ts
228
+ // src/detect/get-plugin-file.ts
66
229
  function heuristicSort(files, projectPath) {
67
230
  const heuristicsBestGuess = `${basename(projectPath)}.php`;
68
231
  const heuristicsBestGuessIndex = files.indexOf(heuristicsBestGuess);
@@ -86,13 +249,13 @@ function getPluginFile(projectPath) {
86
249
  return null;
87
250
  }
88
251
 
89
- // src/wp-playground-wordpress/is-plugin-directory.ts
252
+ // src/detect/is-plugin-directory.ts
90
253
  function isPluginDirectory(projectPath) {
91
254
  const pluginFile = getPluginFile(projectPath);
92
255
  return pluginFile !== null;
93
256
  }
94
257
 
95
- // src/wp-playground-wordpress/is-theme-directory.ts
258
+ // src/detect/is-theme-directory.ts
96
259
  import fs4 from "fs";
97
260
  import path3 from "path";
98
261
  function isThemeDirectory(projectPath) {
@@ -105,43 +268,54 @@ function isThemeDirectory(projectPath) {
105
268
  return themeNameRegex.test(styleCSS);
106
269
  }
107
270
 
108
- // src/wp-playground-wordpress/is-wp-content-directory.ts
271
+ // src/detect/is-wp-app-project.ts
109
272
  import fs5 from "fs";
110
273
  import path4 from "path";
274
+ function isWpAppProject(projectPath) {
275
+ const composerJsonPath = path4.join(projectPath, "composer.json");
276
+ if (!fs5.existsSync(composerJsonPath)) {
277
+ return false;
278
+ }
279
+ try {
280
+ const composerJson = JSON.parse(fs5.readFileSync(composerJsonPath, "utf8"));
281
+ return composerJson.type === "wp-app";
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ // src/detect/is-wp-content-directory.ts
288
+ import fs6 from "fs";
289
+ import path5 from "path";
111
290
  function isWpContentDirectory(projectPath) {
112
- const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
113
- const pluginsExists = fs5.existsSync(path4.join(projectPath, "plugins"));
114
- const themesExists = fs5.existsSync(path4.join(projectPath, "themes"));
291
+ const muPluginsExists = fs6.existsSync(path5.join(projectPath, "mu-plugins"));
292
+ const pluginsExists = fs6.existsSync(path5.join(projectPath, "plugins"));
293
+ const themesExists = fs6.existsSync(path5.join(projectPath, "themes"));
115
294
  if (muPluginsExists || pluginsExists || themesExists) {
116
295
  return true;
117
296
  }
118
297
  return false;
119
298
  }
120
299
 
121
- // src/wp-playground-wordpress/is-wordpress-directory.ts
122
- import fs6 from "fs";
123
- import path5 from "path";
300
+ // src/detect/is-wordpress-directory.ts
301
+ import fs7 from "fs";
302
+ import path6 from "path";
124
303
  function isWordPressDirectory(projectPath) {
125
- return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
304
+ return fs7.existsSync(path6.join(projectPath, "wp-content")) && fs7.existsSync(path6.join(projectPath, "wp-includes")) && fs7.existsSync(path6.join(projectPath, "wp-load.php"));
126
305
  }
127
306
 
128
- // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
129
- import fs7 from "fs";
130
- import path6 from "path";
131
- function isWordPressDevelopDirectory(projectPath) {
132
- const requiredFiles = [
133
- "src",
134
- "src/wp-content",
135
- "src/wp-includes",
136
- "src/wp-load.php",
137
- "build",
138
- "build/wp-content",
139
- "build/wp-includes",
140
- "build/wp-load.php"
141
- ];
142
- return requiredFiles.every(
143
- (file) => fs7.existsSync(path6.join(projectPath, file))
144
- );
307
+ // src/detect/is-classicpress-directory.ts
308
+ import fs8 from "fs";
309
+ import path7 from "path";
310
+ function isClassicPressDirectory(projectPath) {
311
+ if (!isWordPressDirectory(projectPath)) {
312
+ return false;
313
+ }
314
+ const versionFile = path7.join(projectPath, "wp-includes", "version.php");
315
+ if (!fs8.existsSync(versionFile)) {
316
+ return false;
317
+ }
318
+ return fs8.readFileSync(versionFile, "utf8").includes("classicpress_version");
145
319
  }
146
320
 
147
321
  // src/output.ts
@@ -152,24 +326,54 @@ var output = shouldOutput() ? console : null;
152
326
 
153
327
  // src/paths.ts
154
328
  import crypto from "crypto";
155
- import fs8 from "fs";
329
+ import fs9 from "fs";
156
330
  import os from "os";
157
- import path7 from "path";
331
+ import path8 from "path";
158
332
  function getWpAppHome() {
159
- return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
333
+ const home = process.env[WP_APP_HOME_ENV] || path8.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
334
+ cleanupLegacyHome(home);
335
+ return home;
160
336
  }
161
337
  function getCachePath() {
162
- return ensureDir(path7.join(getWpAppHome(), "cache"));
338
+ return ensureDir(path8.join(getWpAppHome(), "cache"));
339
+ }
340
+ function getCorePath(wordPressVersion) {
341
+ const key = wordPressVersion.replace(/[^a-zA-Z0-9.-]/g, "_");
342
+ return path8.join(getWpAppHome(), "core", key);
163
343
  }
164
344
  function getSitePath(projectPath) {
165
- const projectName = path7.basename(path7.resolve(projectPath));
166
- const hash = crypto.createHash("sha1").update(path7.resolve(projectPath)).digest("hex");
167
- return path7.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
345
+ const resolved = path8.resolve(projectPath);
346
+ const identity = process.platform === "win32" ? resolved.toLowerCase() : resolved;
347
+ const projectName = path8.basename(resolved);
348
+ const hash = crypto.createHash("sha1").update(identity).digest("hex");
349
+ return path8.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
350
+ }
351
+ function getProjectLocalPath(projectPath) {
352
+ return path8.join(path8.resolve(projectPath), ".local", "wp-app");
168
353
  }
169
354
  function ensureDir(dir) {
170
- fs8.mkdirSync(dir, { recursive: true });
355
+ fs9.mkdirSync(dir, { recursive: true });
171
356
  return dir;
172
357
  }
358
+ var LEGACY_TOP_LEVEL_DIRS = [
359
+ "wordpress-versions",
360
+ "wp-content",
361
+ "mu-plugins",
362
+ "sqlite-database-integration-main"
363
+ ];
364
+ var legacyCleanupDone = false;
365
+ function cleanupLegacyHome(home) {
366
+ if (legacyCleanupDone) {
367
+ return;
368
+ }
369
+ legacyCleanupDone = true;
370
+ for (const name of LEGACY_TOP_LEVEL_DIRS) {
371
+ const dir = path8.join(home, name);
372
+ if (fs9.existsSync(dir)) {
373
+ fs9.rmSync(dir, { recursive: true, force: true });
374
+ }
375
+ }
376
+ }
173
377
 
174
378
  // src/download.ts
175
379
  function getWordPressVersionUrl(version) {
@@ -188,8 +392,8 @@ function getClassicPressVersionUrl(version) {
188
392
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
189
393
  }
190
394
  async function cachedZip(url, cacheKey) {
191
- const cacheFile = path8.join(getCachePath(), cacheKey);
192
- if (!fs9.existsSync(cacheFile)) {
395
+ const cacheFile = path9.join(getCachePath(), cacheKey);
396
+ if (!fs10.existsSync(cacheFile)) {
193
397
  output?.log(`Downloading ${cacheKey}...`);
194
398
  const response = await fetch(url, { redirect: "follow" });
195
399
  if (!response.ok) {
@@ -198,11 +402,11 @@ async function cachedZip(url, cacheKey) {
198
402
  );
199
403
  }
200
404
  const bytes = Buffer.from(await response.arrayBuffer());
201
- fs9.writeFileSync(`${cacheFile}.partial`, bytes);
202
- fs9.renameSync(`${cacheFile}.partial`, cacheFile);
405
+ fs10.writeFileSync(`${cacheFile}.partial`, bytes);
406
+ fs10.renameSync(`${cacheFile}.partial`, cacheFile);
203
407
  output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
204
408
  }
205
- return new File([fs9.readFileSync(cacheFile)], cacheKey);
409
+ return new File([fs10.readFileSync(cacheFile)], cacheKey);
206
410
  }
207
411
  async function getCoreZip(version) {
208
412
  if (isClassicPressVersion(version)) {
@@ -216,43 +420,71 @@ async function getSqliteIntegrationZip() {
216
420
 
217
421
  // src/engine.ts
218
422
  async function startWPApp(options) {
219
- const projectPath = path9.resolve(options.projectPath);
423
+ const projectPath = path10.resolve(options.projectPath);
220
424
  const mode = options.mode;
221
425
  output?.log(`directory: ${options.projectPath}`);
222
426
  output?.log(`mode: ${mode}`);
223
427
  output?.log(`php: ${options.phpVersion}`);
224
- const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
428
+ const sharesCore = mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */ || mode === "plugin" /* PLUGIN */ || mode === "wp-content" /* WP_CONTENT */;
429
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ || mode === "classicpress" /* CLASSICPRESS */ ? projectPath : ensureDir(getCorePath(options.wordPressVersion));
430
+ const projectDataRoot = sharesCore ? ensureDir(
431
+ mode === "wp-app" /* WP_APP */ ? getProjectLocalPath(projectPath) : getSitePath(projectPath)
432
+ ) : null;
225
433
  const isWordPressBacked = mode !== "index" /* INDEX */;
226
- const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
434
+ const freshCore = isWordPressBacked && !fs11.existsSync(path10.join(hostDocroot, "wp-load.php"));
435
+ const freshSite = sharesCore ? !fs11.existsSync(path10.join(projectDataRoot, "database", ".ht.sqlite")) : freshCore;
227
436
  if (isWordPressBacked) {
228
437
  output?.log(`wp: ${options.wordPressVersion}`);
229
- output?.log(`site data: ${hostDocroot}`);
230
- }
231
- const requestHandler = await bootWordPressAndRequestHandler({
232
- siteUrl: options.absoluteUrl,
233
- documentRoot: DOCROOT,
234
- phpVersion: options.phpVersion,
235
- maxPhpInstances: 1,
236
- sapiName: "cli",
237
- cookieStore: false,
238
- createPhpRuntime: () => loadNodeRuntime(options.phpVersion, {
239
- emscriptenOptions: { processId: process.pid }
240
- }),
241
- hooks: {
242
- beforeWordPressFiles: async (php2) => {
243
- php2.mkdir(DOCROOT);
244
- await php2.mount(
245
- DOCROOT,
246
- createNodeFsMountHandler(hostDocroot)
247
- );
248
- }
249
- },
250
- wordPressZip: freshSite ? await getCoreZip(options.wordPressVersion) : void 0,
251
- sqliteIntegrationPluginZip: isWordPressBacked ? await getSqliteIntegrationZip() : void 0,
252
- dataSqlPath: isWordPressBacked ? `${DOCROOT}/wp-content/database/.ht.sqlite` : void 0,
253
- wordpressInstallMode: isWordPressBacked ? "install-from-existing-files-if-needed" : "do-not-attempt-installing"
254
- });
255
- const php = await requestHandler.getPrimaryPhp();
438
+ output?.log(`site data: ${projectDataRoot ?? hostDocroot}`);
439
+ }
440
+ const php = await createPhp(options.phpVersion);
441
+ php.mkdir(DOCROOT);
442
+ await php.mount(DOCROOT, createNodeFsMountHandler(hostDocroot));
443
+ const requestHandler = createRequestHandler(
444
+ php,
445
+ DOCROOT,
446
+ options.absoluteUrl
447
+ );
448
+ if (freshCore) {
449
+ await installCore(
450
+ php,
451
+ await getCoreZip(options.wordPressVersion),
452
+ DOCROOT
453
+ );
454
+ }
455
+ if (isWordPressBacked) {
456
+ if (!php.fileExists(`${DOCROOT}/wp-content/db.php`)) {
457
+ await installSqliteIntegration(
458
+ php,
459
+ await getSqliteIntegrationZip(),
460
+ DOCROOT
461
+ );
462
+ }
463
+ }
464
+ if (projectDataRoot) {
465
+ const databaseDir = ensureDir(path10.join(projectDataRoot, "database"));
466
+ const uploadsDir = ensureDir(path10.join(projectDataRoot, "uploads"));
467
+ php.mkdir(`${DOCROOT}/wp-content/database`);
468
+ await php.mount(
469
+ `${DOCROOT}/wp-content/database`,
470
+ createNodeFsMountHandler(databaseDir)
471
+ );
472
+ php.mkdir(`${DOCROOT}/wp-content/uploads`);
473
+ await php.mount(
474
+ `${DOCROOT}/wp-content/uploads`,
475
+ createNodeFsMountHandler(uploadsDir)
476
+ );
477
+ }
478
+ if (isWordPressBacked) {
479
+ defineSiteConstants(
480
+ php,
481
+ options.absoluteUrl,
482
+ `${DOCROOT}/wp-content/database`
483
+ );
484
+ }
485
+ if (freshSite) {
486
+ await runInstaller(requestHandler);
487
+ }
256
488
  await applyProjectMounts(php, mode, projectPath);
257
489
  if (freshSite) {
258
490
  await activateProject(php, mode, projectPath);
@@ -260,8 +492,9 @@ async function startWPApp(options) {
260
492
  return { requestHandler, php, options, freshSite };
261
493
  }
262
494
  async function applyProjectMounts(php, mode, projectPath) {
263
- const projectName = path9.basename(projectPath);
495
+ const projectName = path10.basename(projectPath);
264
496
  switch (mode) {
497
+ case "wp-app" /* WP_APP */:
265
498
  case "theme" /* THEME */:
266
499
  await mountAt(
267
500
  php,
@@ -277,13 +510,13 @@ async function applyProjectMounts(php, mode, projectPath) {
277
510
  );
278
511
  break;
279
512
  case "wp-content" /* WP_CONTENT */:
280
- for (const entry of fs10.readdirSync(projectPath)) {
513
+ for (const entry of fs11.readdirSync(projectPath)) {
281
514
  if (entry === "index.php") {
282
515
  continue;
283
516
  }
284
517
  await mountAt(
285
518
  php,
286
- path9.join(projectPath, entry),
519
+ path10.join(projectPath, entry),
287
520
  `${DOCROOT}/wp-content/${entry}`
288
521
  );
289
522
  }
@@ -293,14 +526,14 @@ async function applyProjectMounts(php, mode, projectPath) {
293
526
  }
294
527
  }
295
528
  async function mountAt(php, hostPath, vfsPath) {
296
- if (fs10.statSync(hostPath).isDirectory()) {
529
+ if (fs11.statSync(hostPath).isDirectory()) {
297
530
  php.mkdir(vfsPath);
298
531
  }
299
532
  await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
300
533
  }
301
534
  async function activateProject(php, mode, projectPath) {
302
- const projectName = path9.basename(projectPath);
303
- if (mode === "theme" /* THEME */) {
535
+ const projectName = path10.basename(projectPath);
536
+ if (mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */) {
304
537
  await runWordPressCode(
305
538
  php,
306
539
  `switch_theme(${phpString(projectName)});`
@@ -327,15 +560,17 @@ async function activateProject(php, mode, projectPath) {
327
560
  }
328
561
  }
329
562
  async function runWordPressCode(php, code) {
330
- const response = await php.run({
331
- code: `<?php
563
+ try {
564
+ await runPhp(
565
+ php,
566
+ `
332
567
  require ${phpString(`${DOCROOT}/wp-load.php`)};
333
568
  require_once ${phpString(`${DOCROOT}/wp-admin/includes/plugin.php`)};
334
569
  ${code}
335
- `
336
- });
337
- if (response.exitCode !== 0) {
338
- output?.error(`Activation step failed: ${response.errors}`);
570
+ `
571
+ );
572
+ } catch (error) {
573
+ output?.error(`Activation step failed: ${error.message}`);
339
574
  }
340
575
  }
341
576
  function phpString(value) {
@@ -343,11 +578,11 @@ function phpString(value) {
343
578
  }
344
579
  function findPluginFile(projectPath) {
345
580
  const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
346
- for (const file of fs10.readdirSync(projectPath)) {
581
+ for (const file of fs11.readdirSync(projectPath)) {
347
582
  if (!file.endsWith(".php")) {
348
583
  continue;
349
584
  }
350
- const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
585
+ const content = fs11.readFileSync(path10.join(projectPath, file), "utf8");
351
586
  if (pluginNameRegex.test(content)) {
352
587
  return file;
353
588
  }
@@ -355,8 +590,10 @@ function findPluginFile(projectPath) {
355
590
  return null;
356
591
  }
357
592
  function inferMode(projectPath) {
358
- if (isWordPressDevelopDirectory(projectPath)) {
359
- return "wordpress-develop" /* WORDPRESS_DEVELOP */;
593
+ if (isWpAppProject(projectPath)) {
594
+ return "wp-app" /* WP_APP */;
595
+ } else if (isClassicPressDirectory(projectPath)) {
596
+ return "classicpress" /* CLASSICPRESS */;
360
597
  } else if (isWordPressDirectory(projectPath)) {
361
598
  return "wordpress" /* WORDPRESS */;
362
599
  } else if (isWpContentDirectory(projectPath)) {
@@ -368,7 +605,7 @@ function inferMode(projectPath) {
368
605
  } else if (hasIndexFile(projectPath)) {
369
606
  return "index" /* INDEX */;
370
607
  }
371
- return "playground" /* PLAYGROUND */;
608
+ return null;
372
609
  }
373
610
 
374
611
  // src/port-finder.ts
@@ -399,11 +636,6 @@ var PortFinder = class _PortFinder {
399
636
  });
400
637
  });
401
638
  }
402
- /**
403
- * Returns the first available open port, caching and reusing it for subsequent calls.
404
- *
405
- * @returns {Promise<number>} A promise that resolves to the open port number.
406
- */
407
639
  async getOpenPort() {
408
640
  if (this.#openPort) {
409
641
  return this.#openPort;
@@ -455,7 +687,11 @@ async function getWpAppConfig(args) {
455
687
  }
456
688
  });
457
689
  if (!options.mode || options.mode === "auto") {
458
- options.mode = inferMode(options.projectPath);
690
+ const inferred = inferMode(options.projectPath);
691
+ if (!inferred) {
692
+ process.exit(1);
693
+ }
694
+ options.mode = inferred;
459
695
  }
460
696
  if (!options.absoluteUrl) {
461
697
  options.absoluteUrl = await getAbsoluteURL();
@@ -474,12 +710,12 @@ async function getWpAppConfig(args) {
474
710
  }
475
711
 
476
712
  // src/start-server.ts
477
- import fs11 from "fs";
713
+ import fs12 from "fs";
478
714
  import { Readable } from "stream";
479
715
  import { pipeline } from "stream/promises";
480
716
  import express from "express";
481
717
  async function startServer(options = {}) {
482
- if (!fs11.existsSync(options.projectPath)) {
718
+ if (!fs12.existsSync(options.projectPath)) {
483
719
  throw new Error(
484
720
  `The given path "${options.projectPath}" does not exist.`
485
721
  );