@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/main.js CHANGED
@@ -1,59 +1,527 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/exec-command.ts
17
+ import { execFileSync } from "child_process";
18
+ function runCommand(command, args, options) {
19
+ return execFileSync(command, args, {
20
+ ...options,
21
+ shell: process.platform === "win32"
22
+ });
23
+ }
24
+ var init_exec_command = __esm({
25
+ "src/exec-command.ts"() {
26
+ }
27
+ });
28
+
29
+ // src/create.ts
30
+ var create_exports = {};
31
+ __export(create_exports, {
32
+ runCreateCommand: () => runCreateCommand
33
+ });
34
+ import fs14 from "fs";
35
+ import path12 from "path";
36
+ import readline from "readline/promises";
37
+ async function ask(question, defaultValue = "") {
38
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
39
+ const answer = (await rl.question(`? ${question}${suffix}: `)).trim();
40
+ return answer || defaultValue;
41
+ }
42
+ async function askChoice(question, choices, defaultIndex = 0) {
43
+ console.log(`? ${question}`);
44
+ choices.forEach((choice, i) => {
45
+ const marker = i === defaultIndex ? ">" : " ";
46
+ console.log(` ${marker} ${i + 1}) ${choice}`);
47
+ });
48
+ const answer = (await rl.question(` Choice (${defaultIndex + 1}): `)).trim();
49
+ const index = answer ? parseInt(answer, 10) - 1 : defaultIndex;
50
+ return choices[index] ?? choices[defaultIndex];
51
+ }
52
+ async function askPassword(question, defaultValue) {
53
+ if (!process.stdin.isTTY) {
54
+ return defaultValue;
55
+ }
56
+ process.stdout.write(`? ${question} (${defaultValue}): `);
57
+ return new Promise((resolve) => {
58
+ let value = "";
59
+ process.stdin.setRawMode(true);
60
+ process.stdin.resume();
61
+ process.stdin.setEncoding("utf8");
62
+ const onData = (char) => {
63
+ if (char === "\r" || char === "\n") {
64
+ process.stdin.setRawMode(false);
65
+ process.stdin.pause();
66
+ process.stdin.removeListener("data", onData);
67
+ process.stdout.write("\n");
68
+ resolve(value || defaultValue);
69
+ } else if (char === CTRL_C) {
70
+ process.stdin.setRawMode(false);
71
+ process.exit(1);
72
+ } else if (char === BACKSPACE_DEL || char === BACKSPACE_BS) {
73
+ if (value.length > 0) {
74
+ value = value.slice(0, -1);
75
+ process.stdout.write("\b \b");
76
+ }
77
+ } else if (char.charCodeAt(0) >= 32) {
78
+ value += char;
79
+ process.stdout.write("*");
80
+ }
81
+ };
82
+ process.stdin.on("data", onData);
83
+ });
84
+ }
85
+ function toPascalCase(value) {
86
+ return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
87
+ }
88
+ async function collectAnswers() {
89
+ const projectName = await ask("Project name", "my-app");
90
+ const author = await ask("Author (composer vendor)", "my-name");
91
+ const cssTool = (await askChoice("CSS tooling", ["Tailwind", "SCSS"], 0)).toLowerCase().startsWith("tailwind") ? "tailwind" : "scss";
92
+ const npmPackagesRaw = await ask(
93
+ "npm packages (comma-separated, optional)"
94
+ );
95
+ const composerPackagesRaw = await ask(
96
+ "composer packages (comma-separated, optional)"
97
+ );
98
+ const adminUser = await ask("Admin username", "admin");
99
+ const adminPassword = await askPassword("Admin password", "password");
100
+ return {
101
+ projectName,
102
+ author,
103
+ cssTool,
104
+ npmPackages: splitList(npmPackagesRaw),
105
+ composerPackages: splitList(composerPackagesRaw),
106
+ adminUser,
107
+ adminPassword
108
+ };
109
+ }
110
+ function splitList(value) {
111
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
112
+ }
113
+ function printPreview(answers) {
114
+ console.log("\nReview:");
115
+ console.log(` 1) Project name: ${answers.projectName}`);
116
+ console.log(` 2) Author: ${answers.author}`);
117
+ console.log(` 3) CSS tooling: ${answers.cssTool}`);
118
+ console.log(
119
+ ` 4) npm packages: ${answers.npmPackages.join(", ") || "(none)"}`
120
+ );
121
+ console.log(
122
+ ` 5) composer packages: ${answers.composerPackages.join(", ") || "(none)"}`
123
+ );
124
+ console.log(` 6) Admin username: ${answers.adminUser}`);
125
+ console.log(` 7) Admin password: ${"*".repeat(answers.adminPassword.length)}`);
126
+ }
127
+ async function editAnswers(answers) {
128
+ for (; ; ) {
129
+ printPreview(answers);
130
+ const choice = await ask(
131
+ "Edit a field (number), or press enter to confirm"
132
+ );
133
+ if (!choice) {
134
+ return answers;
135
+ }
136
+ switch (choice) {
137
+ case "1":
138
+ answers.projectName = await ask("Project name", answers.projectName);
139
+ break;
140
+ case "2":
141
+ answers.author = await ask("Author (composer vendor)", answers.author);
142
+ break;
143
+ case "3":
144
+ answers.cssTool = (await askChoice(
145
+ "CSS tooling",
146
+ ["Tailwind", "SCSS"],
147
+ answers.cssTool === "tailwind" ? 0 : 1
148
+ )).toLowerCase().startsWith("tailwind") ? "tailwind" : "scss";
149
+ break;
150
+ case "4":
151
+ answers.npmPackages = splitList(
152
+ await ask(
153
+ "npm packages (comma-separated, optional)",
154
+ answers.npmPackages.join(", ")
155
+ )
156
+ );
157
+ break;
158
+ case "5":
159
+ answers.composerPackages = splitList(
160
+ await ask(
161
+ "composer packages (comma-separated, optional)",
162
+ answers.composerPackages.join(", ")
163
+ )
164
+ );
165
+ break;
166
+ case "6":
167
+ answers.adminUser = await ask("Admin username", answers.adminUser);
168
+ break;
169
+ case "7":
170
+ answers.adminPassword = await askPassword(
171
+ "Admin password",
172
+ answers.adminPassword
173
+ );
174
+ break;
175
+ default:
176
+ console.log("Not a valid field number.");
177
+ }
178
+ }
179
+ }
180
+ function scaffoldFiles(dir, answers) {
181
+ const namespace = toPascalCase(answers.author) + "\\" + toPascalCase(answers.projectName);
182
+ fs14.mkdirSync(path12.join(dir, "src", "js"), { recursive: true });
183
+ fs14.mkdirSync(path12.join(dir, "assets", "css"), { recursive: true });
184
+ fs14.mkdirSync(path12.join(dir, "assets", "js"), { recursive: true });
185
+ writeJson(path12.join(dir, "composer.json"), {
186
+ name: `${answers.author}/${answers.projectName}`,
187
+ type: "wp-app",
188
+ license: "proprietary",
189
+ require: { php: ">=8.0" },
190
+ autoload: { "psr-4": { [`${namespace}\\`]: "src/php/" } }
191
+ });
192
+ const cssDeps = answers.cssTool === "tailwind" ? { tailwindcss: "^4.3.0", "@tailwindcss/cli": "^4.3.0" } : { sass: "^1.103.0" };
193
+ const cssBuild = answers.cssTool === "tailwind" ? "tailwindcss -i src/css/main.css -o assets/css/main.css --minify" : "sass src/css/main.scss assets/css/main.css --style=compressed";
194
+ const cssWatch = answers.cssTool === "tailwind" ? "tailwindcss -i src/css/main.css -o assets/css/main.css --watch" : "sass src/css/main.scss assets/css/main.css --watch";
195
+ writeJson(path12.join(dir, "package.json"), {
196
+ name: answers.projectName,
197
+ private: true,
198
+ scripts: {
199
+ "build:css": cssBuild,
200
+ "watch:css": cssWatch,
201
+ "build:js": "esbuild src/js/main.js --bundle --minify --outfile=assets/js/main.js",
202
+ "watch:js": "esbuild src/js/main.js --bundle --outfile=assets/js/main.js --watch",
203
+ build: "npm run build:css && npm run build:js"
204
+ },
205
+ devDependencies: { esbuild: "^0.28.0", ...cssDeps }
206
+ });
207
+ fs14.writeFileSync(
208
+ path12.join(dir, ".env"),
209
+ `WP_ADMIN_USER=${answers.adminUser}
210
+ WP_ADMIN_PASSWORD=${answers.adminPassword}
211
+ `
212
+ );
213
+ fs14.writeFileSync(
214
+ path12.join(dir, "style.css"),
215
+ `/*
216
+ Theme Name: ${answers.projectName}
217
+ Author: ${answers.author}
218
+ Version: 0.1.0
219
+ */
220
+ `
221
+ );
222
+ fs14.writeFileSync(
223
+ path12.join(dir, "functions.php"),
224
+ `<?php
225
+ declare(strict_types=1);
226
+
227
+ $autoload = __DIR__ . '/vendor/autoload.php';
228
+ if (file_exists($autoload)) {
229
+ require_once $autoload;
230
+ }
231
+
232
+ add_action('wp_enqueue_scripts', function (): void {
233
+ wp_enqueue_style('${answers.projectName}', get_stylesheet_directory_uri() . '/assets/css/main.css');
234
+ wp_enqueue_script('${answers.projectName}', get_stylesheet_directory_uri() . '/assets/js/main.js', [], false, true);
235
+ });
236
+ `
237
+ );
238
+ fs14.writeFileSync(
239
+ path12.join(dir, "index.php"),
240
+ `<?php
241
+ declare(strict_types=1);
242
+ get_header();
243
+ while (have_posts()) : the_post();
244
+ the_content();
245
+ endwhile;
246
+ get_footer();
247
+ `
248
+ );
249
+ const cssEntry = answers.cssTool === "tailwind" ? '@import "tailwindcss";\n' : "// entry stylesheet\n";
250
+ fs14.mkdirSync(path12.join(dir, "src", "css"), { recursive: true });
251
+ fs14.writeFileSync(
252
+ path12.join(
253
+ dir,
254
+ "src",
255
+ "css",
256
+ answers.cssTool === "tailwind" ? "main.css" : "main.scss"
257
+ ),
258
+ cssEntry
259
+ );
260
+ fs14.writeFileSync(path12.join(dir, "src", "js", "main.js"), "// entry script\n");
261
+ fs14.mkdirSync(path12.join(dir, "src", "php"), { recursive: true });
262
+ }
263
+ function writeJson(filePath, data) {
264
+ fs14.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
265
+ }
266
+ async function runCreateCommand(targetPath) {
267
+ const dir = path12.resolve(targetPath);
268
+ fs14.mkdirSync(dir, { recursive: true });
269
+ let answers = await collectAnswers();
270
+ answers = await editAnswers(answers);
271
+ rl.close();
272
+ scaffoldFiles(dir, answers);
273
+ const run = (cmd, args) => runCommand(cmd, args, { cwd: dir, stdio: "inherit" });
274
+ if (answers.npmPackages.length) {
275
+ console.log("Installing additional npm packages...");
276
+ run("npm", ["install", "--save", ...answers.npmPackages]);
277
+ } else {
278
+ console.log("Installing npm dependencies...");
279
+ run("npm", ["install"]);
280
+ }
281
+ if (answers.composerPackages.length) {
282
+ console.log("Requiring additional composer packages...");
283
+ run("composer", ["require", ...answers.composerPackages]);
284
+ } else {
285
+ console.log("Installing composer dependencies...");
286
+ try {
287
+ run("composer", ["install"]);
288
+ } catch {
289
+ console.log("(composer not available or nothing to install yet \u2014 skipped)");
290
+ }
291
+ }
292
+ console.log(`
293
+ Done. cd ${targetPath} && wp-app start`);
294
+ }
295
+ var rl, CTRL_C, BACKSPACE_DEL, BACKSPACE_BS;
296
+ var init_create = __esm({
297
+ "src/create.ts"() {
298
+ init_exec_command();
299
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
300
+ CTRL_C = "";
301
+ BACKSPACE_DEL = "\x7F";
302
+ BACKSPACE_BS = "\b";
303
+ }
304
+ });
305
+
1
306
  // src/run-cli.ts
2
- import fs13 from "fs";
3
- import path11 from "path";
4
- import yargs from "yargs";
5
- import { hideBin } from "yargs/helpers";
307
+ init_exec_command();
308
+ import fs15 from "fs";
309
+ import path13 from "path";
310
+ import { parseArgs } from "util";
6
311
  import { spawn, execSync } from "child_process";
7
312
 
8
313
  // src/start-server.ts
9
- import fs11 from "fs";
314
+ import fs12 from "fs";
10
315
  import { Readable } from "stream";
11
316
  import { pipeline } from "stream/promises";
12
317
  import express from "express";
13
318
 
14
319
  // src/engine.ts
15
- import fs10 from "fs";
16
- import path9 from "path";
17
- import { createNodeFsMountHandler, loadNodeRuntime } from "@php-wasm/node";
18
- import { bootWordPressAndRequestHandler } from "@wp-playground/wordpress";
320
+ import fs11 from "fs";
321
+ import path10 from "path";
322
+ import { createNodeFsMountHandler } from "@php-wasm/node";
323
+
324
+ // src/boot.ts
325
+ import { rootCertificates } from "tls";
326
+ import { loadNodeRuntime } from "@php-wasm/node";
327
+ import { PHP, PHPRequestHandler, setPhpIniEntries } from "@php-wasm/universal";
328
+ var CA_BUNDLE_PATH = "/internal/ca-bundle.crt";
329
+ async function createPhp(phpVersion) {
330
+ const php = new PHP(
331
+ await loadNodeRuntime(phpVersion, {
332
+ emscriptenOptions: { processId: process.pid }
333
+ })
334
+ );
335
+ php.setSapiName("cli");
336
+ php.mkdir("/internal");
337
+ php.writeFile(CA_BUNDLE_PATH, rootCertificates.join("\n"));
338
+ await setPhpIniEntries(php, {
339
+ "openssl.cafile": CA_BUNDLE_PATH,
340
+ "curl.cainfo": CA_BUNDLE_PATH,
341
+ allow_url_fopen: "1",
342
+ disable_functions: ""
343
+ });
344
+ return php;
345
+ }
346
+ function createRequestHandler(php, documentRoot, absoluteUrl) {
347
+ return new PHPRequestHandler({
348
+ php,
349
+ documentRoot,
350
+ absoluteUrl,
351
+ // Route unmatched URLs (permalinks, admin-ajax) to index.php, not 404.
352
+ getFileNotFoundAction: () => ({
353
+ type: "internal-redirect",
354
+ uri: "/index.php"
355
+ }),
356
+ // Let Set-Cookie pass through to the real HTTP server (express).
357
+ cookieStore: false
358
+ });
359
+ }
360
+ async function runPhp(php, code) {
361
+ const response = await php.run({ code: `<?php ${code}` });
362
+ if (response.exitCode !== 0) {
363
+ throw new Error(response.errors || `PHP exited ${response.exitCode}`);
364
+ }
365
+ return response;
366
+ }
367
+ async function unzipTo(php, zip, destination) {
368
+ const tmpZip = `/internal/upload-${Date.now()}.zip`;
369
+ php.writeFile(tmpZip, new Uint8Array(await zip.arrayBuffer()));
370
+ await runPhp(
371
+ php,
372
+ `
373
+ $zip = new ZipArchive();
374
+ if ($zip->open(${str(tmpZip)}) !== true) exit(1);
375
+ if (!is_dir(${str(destination)})) mkdir(${str(destination)}, 0777, true);
376
+ if (!$zip->extractTo(${str(destination)})) exit(1);
377
+ $zip->close();
378
+ unlink(${str(tmpZip)});
379
+ `
380
+ );
381
+ }
382
+ async function installCore(php, coreZip, docroot) {
383
+ const staging = `${docroot}/.wpapp-staging`;
384
+ await unzipTo(php, coreZip, staging);
385
+ await runPhp(
386
+ php,
387
+ `
388
+ function wpapp_core_root($dir) {
389
+ if (file_exists("$dir/wp-config-sample.php")) return $dir;
390
+ foreach (scandir($dir) as $entry) {
391
+ if ($entry === '.' || $entry === '..') continue;
392
+ $candidate = "$dir/$entry";
393
+ if (is_dir($candidate) && file_exists("$candidate/wp-config-sample.php")) {
394
+ return $candidate;
395
+ }
396
+ }
397
+ exit(1);
398
+ }
399
+ function wpapp_rrmdir($dir) {
400
+ foreach (scandir($dir) as $entry) {
401
+ if ($entry === '.' || $entry === '..') continue;
402
+ $path = "$dir/$entry";
403
+ is_dir($path) ? wpapp_rrmdir($path) : unlink($path);
404
+ }
405
+ rmdir($dir);
406
+ }
407
+ $root = wpapp_core_root(${str(staging)});
408
+ foreach (scandir($root) as $entry) {
409
+ if ($entry === '.' || $entry === '..') continue;
410
+ rename("$root/$entry", ${str(docroot)} . "/$entry");
411
+ }
412
+ wpapp_rrmdir(${str(staging)});
413
+ if (!file_exists(${str(docroot)} . '/wp-config.php')) {
414
+ copy(
415
+ ${str(docroot)} . '/wp-config-sample.php',
416
+ ${str(docroot)} . '/wp-config.php'
417
+ );
418
+ }
419
+ `
420
+ );
421
+ }
422
+ async function installSqliteIntegration(php, pluginZip, docroot) {
423
+ const pluginDir = `${docroot}/wp-content/plugins/sqlite-database-integration`;
424
+ const staging = `${docroot}/wp-content/.wpapp-sqlite-staging`;
425
+ await unzipTo(php, pluginZip, staging);
426
+ await runPhp(
427
+ php,
428
+ `
429
+ $root = ${str(staging)};
430
+ $entries = array_values(array_diff(scandir($root), ['.', '..']));
431
+ if (count($entries) === 1 && is_dir("$root/$entries[0]")) {
432
+ $root = "$root/$entries[0]";
433
+ }
434
+ if (!is_dir(${str(pluginDir)})) {
435
+ rename($root, ${str(pluginDir)});
436
+ }
437
+ if (is_dir(${str(staging)})) {
438
+ @rmdir(${str(staging)});
439
+ }
440
+ $dropIn = file_get_contents(${str(pluginDir)} . '/db.copy');
441
+ $dropIn = str_replace(
442
+ '{SQLITE_IMPLEMENTATION_FOLDER_PATH}',
443
+ ${str(pluginDir)},
444
+ $dropIn
445
+ );
446
+ $dropIn = str_replace(
447
+ '{SQLITE_PLUGIN}',
448
+ 'sqlite-database-integration/load.php',
449
+ $dropIn
450
+ );
451
+ file_put_contents(${str(docroot)} . '/wp-content/db.php', $dropIn);
452
+ `
453
+ );
454
+ }
455
+ function defineSiteConstants(php, siteUrl, databaseDir) {
456
+ php.defineConstant("WP_HOME", siteUrl);
457
+ php.defineConstant("WP_SITEURL", siteUrl);
458
+ php.defineConstant("DB_DIR", databaseDir);
459
+ php.defineConstant("DB_FILE", ".ht.sqlite");
460
+ php.defineConstant("WP_SQLITE_AST_DRIVER", true);
461
+ php.defineConstant("DISABLE_WP_CRON", true);
462
+ }
463
+ async function runInstaller(requestHandler) {
464
+ const adminUser = process.env.WP_ADMIN_USER || "admin";
465
+ const adminPassword = process.env.WP_ADMIN_PASSWORD || "password";
466
+ const adminEmail = process.env.WP_ADMIN_EMAIL || "admin@localhost.com";
467
+ const fields = new URLSearchParams({
468
+ language: "en",
469
+ prefix: "wp_",
470
+ weblog_title: "My WordPress Website",
471
+ user_name: adminUser,
472
+ admin_password: adminPassword,
473
+ admin_password2: adminPassword,
474
+ Submit: "Install WordPress",
475
+ pw_weak: "1",
476
+ admin_email: adminEmail
477
+ });
478
+ return requestHandler.request({
479
+ url: "/wp-admin/install.php?step=2",
480
+ method: "POST",
481
+ headers: { "content-type": "application/x-www-form-urlencoded" },
482
+ body: new TextEncoder().encode(fields.toString())
483
+ });
484
+ }
485
+ function str(value) {
486
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
487
+ }
19
488
 
20
489
  // src/constants.ts
21
- import { RecommendedPHPVersion } from "@wp-playground/common";
22
490
  var WP_APP_HIDDEN_FOLDER = ".wp-app";
23
491
  var WP_APP_HOME_ENV = "WP_APP_HOME";
24
492
  var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
25
493
  var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
26
494
  var DEFAULT_PORT = 8881;
27
- var DEFAULT_PHP_VERSION = RecommendedPHPVersion;
495
+ var DEFAULT_PHP_VERSION = "8.3";
28
496
  var DEFAULT_WORDPRESS_VERSION = "latest";
29
497
  var DOCROOT = "/wordpress";
30
498
 
31
499
  // src/download.ts
32
- import fs9 from "fs";
33
- import path8 from "path";
500
+ import fs10 from "fs";
501
+ import path9 from "path";
34
502
 
35
- // src/wp-playground-wordpress/has-index-file.ts
503
+ // src/detect/has-index-file.ts
36
504
  import fs from "fs";
37
505
  import path from "path";
38
506
  function hasIndexFile(projectPath) {
39
507
  return fs.existsSync(path.join(projectPath, "index.php"));
40
508
  }
41
509
 
42
- // src/wp-playground-wordpress/is-valid-wordpress-version.ts
510
+ // src/detect/is-valid-wordpress-version.ts
43
511
  function isValidWordPressVersion(version) {
44
512
  const versionPattern = /^latest$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
45
- const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?)?$/;
513
+ const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?(?:-rc(?:\d+)?)?)?$/;
46
514
  return versionPattern.test(version) || classicPressPattern.test(version);
47
515
  }
48
516
  function isClassicPressVersion(version) {
49
517
  return version === "classicpress" || version.startsWith("classicpress-");
50
518
  }
51
519
 
52
- // src/wp-playground-wordpress/get-plugin-file.ts
520
+ // src/detect/get-plugin-file.ts
53
521
  import fs3 from "fs";
54
522
  import path2, { basename } from "path";
55
523
 
56
- // src/wp-playground-wordpress/read-file-head.ts
524
+ // src/detect/read-file-head.ts
57
525
  import fs2 from "fs";
58
526
  function readFileHead(filePath, length = 8192) {
59
527
  const buffer = Buffer.alloc(length);
@@ -64,7 +532,7 @@ function readFileHead(filePath, length = 8192) {
64
532
  return fileContentBuffer.toString();
65
533
  }
66
534
 
67
- // src/wp-playground-wordpress/get-plugin-file.ts
535
+ // src/detect/get-plugin-file.ts
68
536
  function heuristicSort(files, projectPath) {
69
537
  const heuristicsBestGuess = `${basename(projectPath)}.php`;
70
538
  const heuristicsBestGuessIndex = files.indexOf(heuristicsBestGuess);
@@ -88,13 +556,13 @@ function getPluginFile(projectPath) {
88
556
  return null;
89
557
  }
90
558
 
91
- // src/wp-playground-wordpress/is-plugin-directory.ts
559
+ // src/detect/is-plugin-directory.ts
92
560
  function isPluginDirectory(projectPath) {
93
561
  const pluginFile = getPluginFile(projectPath);
94
562
  return pluginFile !== null;
95
563
  }
96
564
 
97
- // src/wp-playground-wordpress/is-theme-directory.ts
565
+ // src/detect/is-theme-directory.ts
98
566
  import fs4 from "fs";
99
567
  import path3 from "path";
100
568
  function isThemeDirectory(projectPath) {
@@ -107,43 +575,54 @@ function isThemeDirectory(projectPath) {
107
575
  return themeNameRegex.test(styleCSS);
108
576
  }
109
577
 
110
- // src/wp-playground-wordpress/is-wp-content-directory.ts
578
+ // src/detect/is-wp-app-project.ts
111
579
  import fs5 from "fs";
112
580
  import path4 from "path";
581
+ function isWpAppProject(projectPath) {
582
+ const composerJsonPath = path4.join(projectPath, "composer.json");
583
+ if (!fs5.existsSync(composerJsonPath)) {
584
+ return false;
585
+ }
586
+ try {
587
+ const composerJson = JSON.parse(fs5.readFileSync(composerJsonPath, "utf8"));
588
+ return composerJson.type === "wp-app";
589
+ } catch {
590
+ return false;
591
+ }
592
+ }
593
+
594
+ // src/detect/is-wp-content-directory.ts
595
+ import fs6 from "fs";
596
+ import path5 from "path";
113
597
  function isWpContentDirectory(projectPath) {
114
- const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
115
- const pluginsExists = fs5.existsSync(path4.join(projectPath, "plugins"));
116
- const themesExists = fs5.existsSync(path4.join(projectPath, "themes"));
598
+ const muPluginsExists = fs6.existsSync(path5.join(projectPath, "mu-plugins"));
599
+ const pluginsExists = fs6.existsSync(path5.join(projectPath, "plugins"));
600
+ const themesExists = fs6.existsSync(path5.join(projectPath, "themes"));
117
601
  if (muPluginsExists || pluginsExists || themesExists) {
118
602
  return true;
119
603
  }
120
604
  return false;
121
605
  }
122
606
 
123
- // src/wp-playground-wordpress/is-wordpress-directory.ts
124
- import fs6 from "fs";
125
- import path5 from "path";
607
+ // src/detect/is-wordpress-directory.ts
608
+ import fs7 from "fs";
609
+ import path6 from "path";
126
610
  function isWordPressDirectory(projectPath) {
127
- return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
611
+ return fs7.existsSync(path6.join(projectPath, "wp-content")) && fs7.existsSync(path6.join(projectPath, "wp-includes")) && fs7.existsSync(path6.join(projectPath, "wp-load.php"));
128
612
  }
129
613
 
130
- // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
131
- import fs7 from "fs";
132
- import path6 from "path";
133
- function isWordPressDevelopDirectory(projectPath) {
134
- const requiredFiles = [
135
- "src",
136
- "src/wp-content",
137
- "src/wp-includes",
138
- "src/wp-load.php",
139
- "build",
140
- "build/wp-content",
141
- "build/wp-includes",
142
- "build/wp-load.php"
143
- ];
144
- return requiredFiles.every(
145
- (file) => fs7.existsSync(path6.join(projectPath, file))
146
- );
614
+ // src/detect/is-classicpress-directory.ts
615
+ import fs8 from "fs";
616
+ import path7 from "path";
617
+ function isClassicPressDirectory(projectPath) {
618
+ if (!isWordPressDirectory(projectPath)) {
619
+ return false;
620
+ }
621
+ const versionFile = path7.join(projectPath, "wp-includes", "version.php");
622
+ if (!fs8.existsSync(versionFile)) {
623
+ return false;
624
+ }
625
+ return fs8.readFileSync(versionFile, "utf8").includes("classicpress_version");
147
626
  }
148
627
 
149
628
  // src/output.ts
@@ -154,24 +633,54 @@ var output = shouldOutput() ? console : null;
154
633
 
155
634
  // src/paths.ts
156
635
  import crypto from "crypto";
157
- import fs8 from "fs";
636
+ import fs9 from "fs";
158
637
  import os from "os";
159
- import path7 from "path";
638
+ import path8 from "path";
160
639
  function getWpAppHome() {
161
- return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
640
+ const home = process.env[WP_APP_HOME_ENV] || path8.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
641
+ cleanupLegacyHome(home);
642
+ return home;
162
643
  }
163
644
  function getCachePath() {
164
- return ensureDir(path7.join(getWpAppHome(), "cache"));
645
+ return ensureDir(path8.join(getWpAppHome(), "cache"));
646
+ }
647
+ function getCorePath(wordPressVersion) {
648
+ const key = wordPressVersion.replace(/[^a-zA-Z0-9.-]/g, "_");
649
+ return path8.join(getWpAppHome(), "core", key);
165
650
  }
166
651
  function getSitePath(projectPath) {
167
- const projectName = path7.basename(path7.resolve(projectPath));
168
- const hash = crypto.createHash("sha1").update(path7.resolve(projectPath)).digest("hex");
169
- return path7.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
652
+ const resolved = path8.resolve(projectPath);
653
+ const identity = process.platform === "win32" ? resolved.toLowerCase() : resolved;
654
+ const projectName = path8.basename(resolved);
655
+ const hash = crypto.createHash("sha1").update(identity).digest("hex");
656
+ return path8.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
657
+ }
658
+ function getProjectLocalPath(projectPath) {
659
+ return path8.join(path8.resolve(projectPath), ".local", "wp-app");
170
660
  }
171
661
  function ensureDir(dir) {
172
- fs8.mkdirSync(dir, { recursive: true });
662
+ fs9.mkdirSync(dir, { recursive: true });
173
663
  return dir;
174
664
  }
665
+ var LEGACY_TOP_LEVEL_DIRS = [
666
+ "wordpress-versions",
667
+ "wp-content",
668
+ "mu-plugins",
669
+ "sqlite-database-integration-main"
670
+ ];
671
+ var legacyCleanupDone = false;
672
+ function cleanupLegacyHome(home) {
673
+ if (legacyCleanupDone) {
674
+ return;
675
+ }
676
+ legacyCleanupDone = true;
677
+ for (const name of LEGACY_TOP_LEVEL_DIRS) {
678
+ const dir = path8.join(home, name);
679
+ if (fs9.existsSync(dir)) {
680
+ fs9.rmSync(dir, { recursive: true, force: true });
681
+ }
682
+ }
683
+ }
175
684
 
176
685
  // src/download.ts
177
686
  function getWordPressVersionUrl(version) {
@@ -190,8 +699,8 @@ function getClassicPressVersionUrl(version) {
190
699
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
191
700
  }
192
701
  async function cachedZip(url, cacheKey) {
193
- const cacheFile = path8.join(getCachePath(), cacheKey);
194
- if (!fs9.existsSync(cacheFile)) {
702
+ const cacheFile = path9.join(getCachePath(), cacheKey);
703
+ if (!fs10.existsSync(cacheFile)) {
195
704
  output?.log(`Downloading ${cacheKey}...`);
196
705
  const response = await fetch(url, { redirect: "follow" });
197
706
  if (!response.ok) {
@@ -200,11 +709,11 @@ async function cachedZip(url, cacheKey) {
200
709
  );
201
710
  }
202
711
  const bytes = Buffer.from(await response.arrayBuffer());
203
- fs9.writeFileSync(`${cacheFile}.partial`, bytes);
204
- fs9.renameSync(`${cacheFile}.partial`, cacheFile);
712
+ fs10.writeFileSync(`${cacheFile}.partial`, bytes);
713
+ fs10.renameSync(`${cacheFile}.partial`, cacheFile);
205
714
  output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
206
715
  }
207
- return new File([fs9.readFileSync(cacheFile)], cacheKey);
716
+ return new File([fs10.readFileSync(cacheFile)], cacheKey);
208
717
  }
209
718
  async function getCoreZip(version) {
210
719
  if (isClassicPressVersion(version)) {
@@ -255,11 +764,6 @@ var PortFinder = class _PortFinder {
255
764
  });
256
765
  });
257
766
  }
258
- /**
259
- * Returns the first available open port, caching and reusing it for subsequent calls.
260
- *
261
- * @returns {Promise<number>} A promise that resolves to the open port number.
262
- */
263
767
  async getOpenPort() {
264
768
  if (this.#openPort) {
265
769
  return this.#openPort;
@@ -311,7 +815,11 @@ async function getWpAppConfig(args) {
311
815
  }
312
816
  });
313
817
  if (!options.mode || options.mode === "auto") {
314
- options.mode = inferMode(options.projectPath);
818
+ const inferred = inferMode(options.projectPath);
819
+ if (!inferred) {
820
+ process.exit(1);
821
+ }
822
+ options.mode = inferred;
315
823
  }
316
824
  if (!options.absoluteUrl) {
317
825
  options.absoluteUrl = await getAbsoluteURL();
@@ -331,43 +839,71 @@ async function getWpAppConfig(args) {
331
839
 
332
840
  // src/engine.ts
333
841
  async function startWPApp(options) {
334
- const projectPath = path9.resolve(options.projectPath);
842
+ const projectPath = path10.resolve(options.projectPath);
335
843
  const mode = options.mode;
336
844
  output?.log(`directory: ${options.projectPath}`);
337
845
  output?.log(`mode: ${mode}`);
338
846
  output?.log(`php: ${options.phpVersion}`);
339
- const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
847
+ const sharesCore = mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */ || mode === "plugin" /* PLUGIN */ || mode === "wp-content" /* WP_CONTENT */;
848
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ || mode === "classicpress" /* CLASSICPRESS */ ? projectPath : ensureDir(getCorePath(options.wordPressVersion));
849
+ const projectDataRoot = sharesCore ? ensureDir(
850
+ mode === "wp-app" /* WP_APP */ ? getProjectLocalPath(projectPath) : getSitePath(projectPath)
851
+ ) : null;
340
852
  const isWordPressBacked = mode !== "index" /* INDEX */;
341
- const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
853
+ const freshCore = isWordPressBacked && !fs11.existsSync(path10.join(hostDocroot, "wp-load.php"));
854
+ const freshSite = sharesCore ? !fs11.existsSync(path10.join(projectDataRoot, "database", ".ht.sqlite")) : freshCore;
342
855
  if (isWordPressBacked) {
343
856
  output?.log(`wp: ${options.wordPressVersion}`);
344
- output?.log(`site data: ${hostDocroot}`);
345
- }
346
- const requestHandler = await bootWordPressAndRequestHandler({
347
- siteUrl: options.absoluteUrl,
348
- documentRoot: DOCROOT,
349
- phpVersion: options.phpVersion,
350
- maxPhpInstances: 1,
351
- sapiName: "cli",
352
- cookieStore: false,
353
- createPhpRuntime: () => loadNodeRuntime(options.phpVersion, {
354
- emscriptenOptions: { processId: process.pid }
355
- }),
356
- hooks: {
357
- beforeWordPressFiles: async (php2) => {
358
- php2.mkdir(DOCROOT);
359
- await php2.mount(
360
- DOCROOT,
361
- createNodeFsMountHandler(hostDocroot)
362
- );
363
- }
364
- },
365
- wordPressZip: freshSite ? await getCoreZip(options.wordPressVersion) : void 0,
366
- sqliteIntegrationPluginZip: isWordPressBacked ? await getSqliteIntegrationZip() : void 0,
367
- dataSqlPath: isWordPressBacked ? `${DOCROOT}/wp-content/database/.ht.sqlite` : void 0,
368
- wordpressInstallMode: isWordPressBacked ? "install-from-existing-files-if-needed" : "do-not-attempt-installing"
369
- });
370
- const php = await requestHandler.getPrimaryPhp();
857
+ output?.log(`site data: ${projectDataRoot ?? hostDocroot}`);
858
+ }
859
+ const php = await createPhp(options.phpVersion);
860
+ php.mkdir(DOCROOT);
861
+ await php.mount(DOCROOT, createNodeFsMountHandler(hostDocroot));
862
+ const requestHandler = createRequestHandler(
863
+ php,
864
+ DOCROOT,
865
+ options.absoluteUrl
866
+ );
867
+ if (freshCore) {
868
+ await installCore(
869
+ php,
870
+ await getCoreZip(options.wordPressVersion),
871
+ DOCROOT
872
+ );
873
+ }
874
+ if (isWordPressBacked) {
875
+ if (!php.fileExists(`${DOCROOT}/wp-content/db.php`)) {
876
+ await installSqliteIntegration(
877
+ php,
878
+ await getSqliteIntegrationZip(),
879
+ DOCROOT
880
+ );
881
+ }
882
+ }
883
+ if (projectDataRoot) {
884
+ const databaseDir = ensureDir(path10.join(projectDataRoot, "database"));
885
+ const uploadsDir = ensureDir(path10.join(projectDataRoot, "uploads"));
886
+ php.mkdir(`${DOCROOT}/wp-content/database`);
887
+ await php.mount(
888
+ `${DOCROOT}/wp-content/database`,
889
+ createNodeFsMountHandler(databaseDir)
890
+ );
891
+ php.mkdir(`${DOCROOT}/wp-content/uploads`);
892
+ await php.mount(
893
+ `${DOCROOT}/wp-content/uploads`,
894
+ createNodeFsMountHandler(uploadsDir)
895
+ );
896
+ }
897
+ if (isWordPressBacked) {
898
+ defineSiteConstants(
899
+ php,
900
+ options.absoluteUrl,
901
+ `${DOCROOT}/wp-content/database`
902
+ );
903
+ }
904
+ if (freshSite) {
905
+ await runInstaller(requestHandler);
906
+ }
371
907
  await applyProjectMounts(php, mode, projectPath);
372
908
  if (freshSite) {
373
909
  await activateProject(php, mode, projectPath);
@@ -375,8 +911,9 @@ async function startWPApp(options) {
375
911
  return { requestHandler, php, options, freshSite };
376
912
  }
377
913
  async function applyProjectMounts(php, mode, projectPath) {
378
- const projectName = path9.basename(projectPath);
914
+ const projectName = path10.basename(projectPath);
379
915
  switch (mode) {
916
+ case "wp-app" /* WP_APP */:
380
917
  case "theme" /* THEME */:
381
918
  await mountAt(
382
919
  php,
@@ -392,13 +929,13 @@ async function applyProjectMounts(php, mode, projectPath) {
392
929
  );
393
930
  break;
394
931
  case "wp-content" /* WP_CONTENT */:
395
- for (const entry of fs10.readdirSync(projectPath)) {
932
+ for (const entry of fs11.readdirSync(projectPath)) {
396
933
  if (entry === "index.php") {
397
934
  continue;
398
935
  }
399
936
  await mountAt(
400
937
  php,
401
- path9.join(projectPath, entry),
938
+ path10.join(projectPath, entry),
402
939
  `${DOCROOT}/wp-content/${entry}`
403
940
  );
404
941
  }
@@ -408,14 +945,14 @@ async function applyProjectMounts(php, mode, projectPath) {
408
945
  }
409
946
  }
410
947
  async function mountAt(php, hostPath, vfsPath) {
411
- if (fs10.statSync(hostPath).isDirectory()) {
948
+ if (fs11.statSync(hostPath).isDirectory()) {
412
949
  php.mkdir(vfsPath);
413
950
  }
414
951
  await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
415
952
  }
416
953
  async function activateProject(php, mode, projectPath) {
417
- const projectName = path9.basename(projectPath);
418
- if (mode === "theme" /* THEME */) {
954
+ const projectName = path10.basename(projectPath);
955
+ if (mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */) {
419
956
  await runWordPressCode(
420
957
  php,
421
958
  `switch_theme(${phpString(projectName)});`
@@ -442,15 +979,17 @@ async function activateProject(php, mode, projectPath) {
442
979
  }
443
980
  }
444
981
  async function runWordPressCode(php, code) {
445
- const response = await php.run({
446
- code: `<?php
982
+ try {
983
+ await runPhp(
984
+ php,
985
+ `
447
986
  require ${phpString(`${DOCROOT}/wp-load.php`)};
448
987
  require_once ${phpString(`${DOCROOT}/wp-admin/includes/plugin.php`)};
449
988
  ${code}
450
- `
451
- });
452
- if (response.exitCode !== 0) {
453
- output?.error(`Activation step failed: ${response.errors}`);
989
+ `
990
+ );
991
+ } catch (error) {
992
+ output?.error(`Activation step failed: ${error.message}`);
454
993
  }
455
994
  }
456
995
  function phpString(value) {
@@ -458,11 +997,11 @@ function phpString(value) {
458
997
  }
459
998
  function findPluginFile(projectPath) {
460
999
  const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
461
- for (const file of fs10.readdirSync(projectPath)) {
1000
+ for (const file of fs11.readdirSync(projectPath)) {
462
1001
  if (!file.endsWith(".php")) {
463
1002
  continue;
464
1003
  }
465
- const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
1004
+ const content = fs11.readFileSync(path10.join(projectPath, file), "utf8");
466
1005
  if (pluginNameRegex.test(content)) {
467
1006
  return file;
468
1007
  }
@@ -470,8 +1009,10 @@ function findPluginFile(projectPath) {
470
1009
  return null;
471
1010
  }
472
1011
  function inferMode(projectPath) {
473
- if (isWordPressDevelopDirectory(projectPath)) {
474
- return "wordpress-develop" /* WORDPRESS_DEVELOP */;
1012
+ if (isWpAppProject(projectPath)) {
1013
+ return "wp-app" /* WP_APP */;
1014
+ } else if (isClassicPressDirectory(projectPath)) {
1015
+ return "classicpress" /* CLASSICPRESS */;
475
1016
  } else if (isWordPressDirectory(projectPath)) {
476
1017
  return "wordpress" /* WORDPRESS */;
477
1018
  } else if (isWpContentDirectory(projectPath)) {
@@ -483,12 +1024,12 @@ function inferMode(projectPath) {
483
1024
  } else if (hasIndexFile(projectPath)) {
484
1025
  return "index" /* INDEX */;
485
1026
  }
486
- return "playground" /* PLAYGROUND */;
1027
+ return null;
487
1028
  }
488
1029
 
489
1030
  // src/start-server.ts
490
1031
  async function startServer(options = {}) {
491
- if (!fs11.existsSync(options.projectPath)) {
1032
+ if (!fs12.existsSync(options.projectPath)) {
492
1033
  throw new Error(
493
1034
  `The given path "${options.projectPath}" does not exist.`
494
1035
  );
@@ -559,23 +1100,23 @@ var parseHeaders = (req) => {
559
1100
  };
560
1101
 
561
1102
  // src/execute-php.ts
562
- import fs12 from "fs";
563
- import path10 from "path";
1103
+ import fs13 from "fs";
1104
+ import path11 from "path";
564
1105
  import { loadNodeRuntime as loadNodeRuntime2 } from "@php-wasm/node";
565
- import { PHP } from "@php-wasm/universal";
1106
+ import { PHP as PHP2 } from "@php-wasm/universal";
566
1107
  async function executePHP(phpArgs, options) {
567
1108
  const args = phpArgs.filter((arg) => arg !== "php" && arg !== "--");
568
1109
  let code;
569
1110
  if (args[0] === "-r" && typeof args[1] === "string") {
570
1111
  code = `<?php ${args[1]}`;
571
- } else if (args[0] && fs12.existsSync(path10.resolve(args[0]))) {
572
- code = fs12.readFileSync(path10.resolve(args[0]), "utf8");
1112
+ } else if (args[0] && fs13.existsSync(path11.resolve(args[0]))) {
1113
+ code = fs13.readFileSync(path11.resolve(args[0]), "utf8");
573
1114
  } else {
574
1115
  throw new Error(
575
1116
  'Usage: wp-app php -- -r "<code>" | wp-app php -- <script.php>'
576
1117
  );
577
1118
  }
578
- const php = new PHP(
1119
+ const php = new PHP2(
579
1120
  await loadNodeRuntime2(options.phpVersion, {
580
1121
  emscriptenOptions: { processId: process.pid }
581
1122
  })
@@ -591,14 +1132,42 @@ async function executePHP(phpArgs, options) {
591
1132
  // src/run-cli.ts
592
1133
  var MODE_CHOICES = [
593
1134
  "auto",
1135
+ "wp-app",
594
1136
  "plugin",
595
1137
  "theme",
596
1138
  "wordpress",
597
- "wordpress-develop",
1139
+ "classicpress",
598
1140
  "wp-content",
599
- "index",
600
- "playground"
1141
+ "index"
601
1142
  ];
1143
+ var SERVER_OPTIONS = {
1144
+ path: { type: "string" },
1145
+ php: { type: "string" },
1146
+ wp: { type: "string" },
1147
+ port: { type: "string" },
1148
+ mode: { type: "string" }
1149
+ };
1150
+ var HELP = `wp-app <command> [args]
1151
+
1152
+ Commands:
1153
+ wp-app start Start the server
1154
+ wp-app php Run the php command passing the arguments to php cli
1155
+ wp-app build Regenerate the composer autoloader (if vendor/ exists) and run the npm build script, when present
1156
+ wp-app dev Start the server; watch CSS/JS source files and rebuild + refresh on change
1157
+ wp-app install Run npm install and composer install
1158
+ wp-app update Run composer update (optionally scoped to specific packages)
1159
+ wp-app create Scaffold a new wp-app project
1160
+
1161
+ Options:
1162
+ --path=<dir> Path to the PHP or WordPress project. Defaults to the current working directory.
1163
+ --php=<ver> PHP version to use.
1164
+ --wp=<ver> WordPress version to use, e.g. '--wp=6.4'. Use '--wp=classicpress' for ClassicPress.
1165
+ --port=<n> Server port
1166
+ --mode=<mode> Project mode (${MODE_CHOICES.join("|")}). Defaults to auto-detection.
1167
+ --reset (start) Create a new site environment, destroying the old one.
1168
+ --no-open (start/dev) Don't open the site in the default browser.
1169
+ -h, --help Show this help
1170
+ `;
602
1171
  function startSpinner(message) {
603
1172
  process.stdout.write(`${message}...
604
1173
  `);
@@ -611,218 +1180,219 @@ function startSpinner(message) {
611
1180
  }
612
1181
  };
613
1182
  }
614
- function commonParameters(yargs2) {
615
- return yargs2.option("path", {
616
- describe: "Path to the PHP or WordPress project. Defaults to the current working directory.",
617
- type: "string"
618
- }).option("php", {
619
- describe: "PHP version to use.",
620
- type: "string"
621
- });
1183
+ function fail(message) {
1184
+ console.error(`wp-app: ${message}
1185
+ `);
1186
+ console.error(HELP);
1187
+ process.exit(1);
622
1188
  }
623
- function serverParameters(yargs2) {
624
- commonParameters(yargs2);
625
- yargs2.option("wp", {
626
- describe: "WordPress version to use, e.g. '--wp=6.4'. Use '--wp=classicpress' for ClassicPress.",
627
- type: "string"
628
- });
629
- yargs2.option("port", {
630
- describe: "Server port",
631
- type: "number"
632
- });
633
- yargs2.option("mode", {
634
- describe: "Project mode. Defaults to auto-detection from the project directory.",
635
- type: "string",
636
- choices: MODE_CHOICES
637
- });
1189
+ function hasNegatedFlag(args, flag) {
1190
+ return args.includes(`--no-${flag}`);
638
1191
  }
639
1192
  async function runCli() {
640
- return yargs(hideBin(process.argv)).scriptName("wp-app").usage("$0 <cmd> [args]").check(async (argv) => {
641
- if (["build", "dev"].includes(argv._[0])) {
642
- return true;
1193
+ const rawArgs = process.argv.slice(2);
1194
+ if (rawArgs.includes("-h") || rawArgs.includes("--help")) {
1195
+ console.log(HELP);
1196
+ return;
1197
+ }
1198
+ const command = rawArgs[0];
1199
+ if (!command || command.startsWith("-")) {
1200
+ fail("You must provide a valid command");
1201
+ }
1202
+ if (command === "php") {
1203
+ return runPhpCommand(rawArgs);
1204
+ }
1205
+ if (command === "create") {
1206
+ const { runCreateCommand: runCreateCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
1207
+ const target = rawArgs[1] || ".";
1208
+ return runCreateCommand2(target);
1209
+ }
1210
+ const { values } = parseArgs({
1211
+ args: rawArgs.slice(1),
1212
+ options: { ...SERVER_OPTIONS, reset: { type: "boolean" } },
1213
+ allowPositionals: true,
1214
+ strict: false
1215
+ });
1216
+ if (command === "install") {
1217
+ return runInstallCommand(values);
1218
+ }
1219
+ if (command === "update") {
1220
+ return runUpdateCommand(values, rawArgs);
1221
+ }
1222
+ if (values.mode && !MODE_CHOICES.includes(values.mode)) {
1223
+ fail(
1224
+ `Invalid mode "${values.mode}". Choices: ${MODE_CHOICES.join(", ")}`
1225
+ );
1226
+ }
1227
+ const config = {
1228
+ php: values.php,
1229
+ path: values.path
1230
+ };
1231
+ config.wp = values.wp;
1232
+ config.port = values.port ? Number(values.port) : void 0;
1233
+ config.mode = values.mode;
1234
+ try {
1235
+ await getWpAppConfig(config);
1236
+ } catch (error) {
1237
+ fail(error.message);
1238
+ }
1239
+ switch (command) {
1240
+ case "start":
1241
+ return runStartCommand(values, rawArgs);
1242
+ case "build":
1243
+ return runBuildCommand(values);
1244
+ case "dev":
1245
+ return runDevCommand(values, rawArgs);
1246
+ default:
1247
+ fail(`Unknown command "${command}"`);
1248
+ }
1249
+ }
1250
+ async function runStartCommand(values, rawArgs) {
1251
+ const spinner = startSpinner("Starting the server...");
1252
+ try {
1253
+ const options = await getWpAppConfig({
1254
+ path: values.path,
1255
+ php: values.php,
1256
+ wp: values.wp,
1257
+ port: values.port ? Number(values.port) : void 0,
1258
+ mode: values.mode
1259
+ });
1260
+ portFinder.setPort(options.port);
1261
+ if (values.reset) {
1262
+ if (["wordpress", "classicpress", "index"].includes(
1263
+ options.mode
1264
+ )) {
1265
+ output?.log(
1266
+ "--reset only applies to wp-app managed sites; your project files are never touched."
1267
+ );
1268
+ } else {
1269
+ fs15.rmSync(getSitePath(options.projectPath), {
1270
+ recursive: true,
1271
+ force: true,
1272
+ maxRetries: 10,
1273
+ retryDelay: 100
1274
+ });
1275
+ output?.log("Site environment reset.");
1276
+ }
643
1277
  }
644
- const config = {
645
- php: argv.php,
646
- path: argv.path
647
- };
648
- if (argv._[0] !== "php") {
649
- config.wp = argv.wp;
650
- config.port = argv.port;
651
- config.mode = argv.mode;
1278
+ const { url } = await startServer(options);
1279
+ if (!hasNegatedFlag(rawArgs, "open")) {
1280
+ openInDefaultBrowser(url);
652
1281
  }
1282
+ } catch (error) {
1283
+ output?.error(error);
1284
+ spinner.fail(`Failed to start the server: ${error.message}`);
1285
+ }
1286
+ }
1287
+ async function runPhpCommand(rawArgs) {
1288
+ const args = rawArgs.slice(1);
1289
+ const pathIndex = args.findIndex((arg) => arg.startsWith("--path="));
1290
+ const phpIndex = args.findIndex((arg) => arg.startsWith("--php="));
1291
+ try {
1292
+ const options = await getWpAppConfig({
1293
+ path: pathIndex >= 0 ? args[pathIndex].split("=")[1] : void 0,
1294
+ php: phpIndex >= 0 ? args[phpIndex].split("=")[1] : void 0
1295
+ });
1296
+ const phpArgs = args.filter(
1297
+ (arg, i) => i !== pathIndex && i !== phpIndex
1298
+ );
1299
+ await executePHP(phpArgs, options);
1300
+ process.exit(0);
1301
+ } catch (error) {
1302
+ console.error(error);
1303
+ process.exit(error.status || -1);
1304
+ }
1305
+ }
1306
+ async function runBuildCommand(values) {
1307
+ const projectPath = values.path ? path13.resolve(values.path) : process.cwd();
1308
+ if (fs15.existsSync(path13.join(projectPath, "vendor"))) {
653
1309
  try {
654
- await getWpAppConfig(config);
1310
+ execSync("composer dump-autoload --optimize --classmap-authoritative", {
1311
+ cwd: projectPath,
1312
+ stdio: "inherit"
1313
+ });
655
1314
  } catch (error) {
656
- return error.message;
1315
+ console.error(
1316
+ `composer dump-autoload failed: ${error.message}`
1317
+ );
657
1318
  }
658
- return true;
659
- }).command(
660
- "start",
661
- "Start the server",
662
- (yargs2) => {
663
- serverParameters(yargs2);
664
- yargs2.option("reset", {
665
- describe: "Create a new site environment, destroying the old one (WordPress files and database).",
666
- type: "boolean",
667
- default: false
668
- });
669
- yargs2.option("open", {
670
- describe: "Open the site in the default browser.",
671
- type: "boolean",
672
- default: true
673
- });
674
- },
675
- async (argv) => {
676
- const spinner = startSpinner("Starting the server...");
677
- try {
678
- const options = await getWpAppConfig({
679
- path: argv.path,
680
- php: argv.php,
681
- wp: argv.wp,
682
- port: argv.port,
683
- mode: argv.mode
684
- });
685
- portFinder.setPort(options.port);
686
- if (argv.reset) {
687
- if (["wordpress", "wordpress-develop", "index"].includes(
688
- options.mode
689
- )) {
690
- output?.log(
691
- "--reset only applies to wp-app managed sites; your project files are never touched."
692
- );
693
- } else {
694
- fs13.rmSync(getSitePath(options.projectPath), {
695
- recursive: true,
696
- force: true,
697
- maxRetries: 10,
698
- retryDelay: 100
699
- });
700
- output?.log("Site environment reset.");
701
- }
702
- }
703
- const { url } = await startServer(options);
704
- if (argv.open) {
705
- openInDefaultBrowser(url);
706
- }
707
- } catch (error) {
708
- output?.error(error);
709
- spinner.fail(
710
- `Failed to start the server: ${error.message}`
711
- );
712
- }
1319
+ }
1320
+ const packageJsonPath = path13.join(projectPath, "package.json");
1321
+ if (fs15.existsSync(packageJsonPath)) {
1322
+ const pkg = JSON.parse(fs15.readFileSync(packageJsonPath, "utf8"));
1323
+ if (pkg.scripts?.build) {
1324
+ execSync("npm run build", { cwd: projectPath, stdio: "inherit" });
713
1325
  }
714
- ).command(
715
- "php [..args]",
716
- "Run the php command passing the arguments to php cli",
717
- (yargs2) => {
718
- commonParameters(yargs2);
719
- yargs2.strict(false);
720
- },
721
- async (argv) => {
722
- try {
723
- const args = process.argv.slice(2);
724
- const options = await getWpAppConfig({
725
- path: argv.path,
726
- php: argv.php
727
- });
728
- const phpArgs = args.includes("--") ? argv._ : args;
729
- await executePHP(phpArgs, options);
730
- process.exit(0);
731
- } catch (error) {
732
- console.error(error);
733
- process.exit(error.status || -1);
734
- }
1326
+ }
1327
+ console.log("Build complete.");
1328
+ }
1329
+ async function runInstallCommand(values) {
1330
+ const projectPath = values.path ? path13.resolve(values.path) : process.cwd();
1331
+ if (fs15.existsSync(path13.join(projectPath, "package.json"))) {
1332
+ execSync("npm install", { cwd: projectPath, stdio: "inherit" });
1333
+ }
1334
+ if (fs15.existsSync(path13.join(projectPath, "composer.json"))) {
1335
+ execSync("composer install", { cwd: projectPath, stdio: "inherit" });
1336
+ }
1337
+ }
1338
+ async function runUpdateCommand(values, rawArgs) {
1339
+ const projectPath = values.path ? path13.resolve(values.path) : process.cwd();
1340
+ const packages = rawArgs.slice(1).filter((arg) => !arg.startsWith("--"));
1341
+ const args = ["update", ...packages];
1342
+ runCommand("composer", args, { cwd: projectPath, stdio: "inherit" });
1343
+ }
1344
+ var WATCHED_EXTENSIONS = [".css", ".scss", ".js", ".ts", ".jsx", ".tsx"];
1345
+ var IGNORED_DIRS = ["node_modules", "vendor", ".git", "dist", "assets"];
1346
+ async function runDevCommand(values, rawArgs) {
1347
+ const projectPath = path13.resolve(values.path || process.cwd());
1348
+ const spinner = startSpinner("Starting the server...");
1349
+ try {
1350
+ const options = await getWpAppConfig({
1351
+ path: values.path,
1352
+ php: values.php,
1353
+ wp: values.wp,
1354
+ port: values.port ? Number(values.port) : void 0,
1355
+ mode: values.mode
1356
+ });
1357
+ portFinder.setPort(options.port);
1358
+ const { url } = await startServer(options);
1359
+ if (!hasNegatedFlag(rawArgs, "open")) {
1360
+ openInDefaultBrowser(url);
735
1361
  }
736
- ).command(
737
- "build",
738
- "Build the project: composer install plus the npm build script, when present",
739
- (yargs2) => {
740
- commonParameters(yargs2);
741
- },
742
- async (argv) => {
743
- const projectPath = argv.path ? path11.resolve(argv.path) : process.cwd();
744
- console.log(`Building project at ${projectPath}`);
745
- if (fs13.existsSync(path11.join(projectPath, "composer.json"))) {
746
- console.log("Running composer install...");
747
- try {
748
- execSync(
749
- "composer install --no-dev --optimize-autoloader",
750
- { cwd: projectPath, stdio: "inherit" }
751
- );
752
- } catch (error) {
753
- console.error(
754
- `composer install failed: ${error.message}`
755
- );
756
- }
757
- }
758
- const packageJsonPath = path11.join(projectPath, "package.json");
759
- if (fs13.existsSync(packageJsonPath)) {
760
- const pkg = JSON.parse(
761
- fs13.readFileSync(packageJsonPath, "utf8")
762
- );
763
- if (pkg.scripts?.build) {
764
- console.log("Running npm build...");
765
- execSync("npm run build", {
766
- cwd: projectPath,
767
- stdio: "inherit"
768
- });
769
- }
770
- }
771
- console.log("Build complete.");
1362
+ } catch (error) {
1363
+ output?.error(error);
1364
+ spinner.fail(`Failed to start the server: ${error.message}`);
1365
+ return;
1366
+ }
1367
+ const packageJsonPath = path13.join(projectPath, "package.json");
1368
+ const hasBuildScript = fs15.existsSync(packageJsonPath) && JSON.parse(fs15.readFileSync(packageJsonPath, "utf8")).scripts?.build;
1369
+ if (!hasBuildScript) {
1370
+ console.log("Dev mode: no npm build script found, nothing to watch.");
1371
+ return;
1372
+ }
1373
+ let rebuildTimer = null;
1374
+ const rebuild = (filename) => {
1375
+ console.log(`Change detected: ${filename}. Rebuilding...`);
1376
+ try {
1377
+ execSync("npm run build", { cwd: projectPath, stdio: "inherit" });
1378
+ console.log("Rebuild complete. Refresh your browser to see changes.");
1379
+ } catch (error) {
1380
+ console.error(`Rebuild failed: ${error.message}`);
772
1381
  }
773
- ).command(
774
- "dev",
775
- "Start the server and restart it when project files change",
776
- (yargs2) => {
777
- serverParameters(yargs2);
778
- },
779
- async (argv) => {
780
- const selfScript = process.argv[1];
781
- const watchDir = path11.resolve(
782
- argv.path || process.cwd()
783
- );
784
- const IGNORED = ["node_modules", "vendor", ".git", "dist"];
785
- let child = null;
786
- let restartTimer = null;
787
- let firstRun = true;
788
- const startChild = () => {
789
- const args = [selfScript, "start"];
790
- if (argv.path) args.push(`--path=${argv.path}`);
791
- if (argv.php) args.push(`--php=${argv.php}`);
792
- if (argv.wp) args.push(`--wp=${argv.wp}`);
793
- if (argv.port) args.push(`--port=${argv.port}`);
794
- if (argv.mode) args.push(`--mode=${argv.mode}`);
795
- if (!firstRun) args.push("--no-open");
796
- firstRun = false;
797
- child = spawn(process.execPath, args, {
798
- stdio: "inherit"
799
- });
800
- };
801
- startChild();
802
- fs13.watch(
803
- watchDir,
804
- { recursive: true },
805
- (_eventType, filename) => {
806
- if (!filename) return;
807
- const parts = filename.split(/[\\/]/);
808
- if (parts.some(
809
- (part) => IGNORED.includes(part) || part.startsWith(".")
810
- )) {
811
- return;
812
- }
813
- clearTimeout(restartTimer);
814
- restartTimer = setTimeout(() => {
815
- console.log(
816
- `Change detected: ${filename}. Restarting...`
817
- );
818
- child?.kill();
819
- startChild();
820
- }, 500);
821
- }
822
- );
823
- console.log("Dev mode: watching for file changes...");
1382
+ };
1383
+ fs15.watch(projectPath, { recursive: true }, (_eventType, filename) => {
1384
+ if (!filename) return;
1385
+ const parts = filename.split(/[\\/]/);
1386
+ if (parts.some((part) => IGNORED_DIRS.includes(part) || part.startsWith("."))) {
1387
+ return;
824
1388
  }
825
- ).demandCommand(1, "You must provide a valid command").help().alias("h", "help").strict().argv;
1389
+ if (!WATCHED_EXTENSIONS.includes(path13.extname(filename))) {
1390
+ return;
1391
+ }
1392
+ if (rebuildTimer) clearTimeout(rebuildTimer);
1393
+ rebuildTimer = setTimeout(() => rebuild(filename), 300);
1394
+ });
1395
+ console.log("Dev mode: watching CSS/JS source files for changes...");
826
1396
  }
827
1397
  function openInDefaultBrowser(url) {
828
1398
  if (isGitHubCodespace) {
@@ -859,4 +1429,8 @@ if (currentNodeVersion < requiredMajorVersion) {
859
1429
  `You are running Node.js version ${currentNodeVersion}, but this application requires at least Node.js ${requiredMajorVersion}. Please upgrade your Node.js version.`
860
1430
  );
861
1431
  }
1432
+ try {
1433
+ process.loadEnvFile();
1434
+ } catch {
1435
+ }
862
1436
  runCli();