@wp-operations/wp-app 0.1.0 → 0.2.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 +243 -489
  3. package/dist/main.js +340 -574
  4. package/package.json +8 -11
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,29 +9,29 @@ 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, loadNodeRuntime } from "@php-wasm/node";
16
+ import { bootWordPressAndRequestHandler } from "@wp-playground/wordpress";
19
17
 
20
18
  // src/constants.ts
21
- var WP_NOW_HIDDEN_FOLDER = ".wp-app";
22
- var SQLITE_FILENAME = "sqlite-database-integration";
19
+ import { RecommendedPHPVersion } from "@wp-playground/common";
20
+ var WP_APP_HIDDEN_FOLDER = ".wp-app";
21
+ var WP_APP_HOME_ENV = "WP_APP_HOME";
23
22
  var SQLITE_URL = "https://github.com/WordPress/sqlite-database-integration/archive/refs/heads/main.zip";
24
23
  var CLASSICPRESS_LATEST_URL = "https://www.classicpress.net/latest.zip";
25
24
  var DEFAULT_PORT = 8881;
26
- var DEFAULT_PHP_VERSION = "8.0";
25
+ var DEFAULT_PHP_VERSION = RecommendedPHPVersion;
27
26
  var DEFAULT_WORDPRESS_VERSION = "latest";
27
+ var DOCROOT = "/wordpress";
28
28
 
29
29
  // 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";
30
+ import fs9 from "fs";
31
+ import path8 from "path";
35
32
 
36
33
  // src/wp-playground-wordpress/has-index-file.ts
37
- import fs from "fs-extra";
34
+ import fs from "fs";
38
35
  import path from "path";
39
36
  function hasIndexFile(projectPath) {
40
37
  return fs.existsSync(path.join(projectPath, "index.php"));
@@ -51,11 +48,11 @@ function isClassicPressVersion(version) {
51
48
  }
52
49
 
53
50
  // src/wp-playground-wordpress/get-plugin-file.ts
54
- import fs3 from "fs-extra";
51
+ import fs3 from "fs";
55
52
  import path2, { basename } from "path";
56
53
 
57
54
  // src/wp-playground-wordpress/read-file-head.ts
58
- import fs2 from "fs-extra";
55
+ import fs2 from "fs";
59
56
  function readFileHead(filePath, length = 8192) {
60
57
  const buffer = Buffer.alloc(length);
61
58
  const fd = fs2.openSync(filePath, "r");
@@ -96,7 +93,7 @@ function isPluginDirectory(projectPath) {
96
93
  }
97
94
 
98
95
  // src/wp-playground-wordpress/is-theme-directory.ts
99
- import fs4 from "fs-extra";
96
+ import fs4 from "fs";
100
97
  import path3 from "path";
101
98
  function isThemeDirectory(projectPath) {
102
99
  const styleCSSExists = fs4.existsSync(path3.join(projectPath, "style.css"));
@@ -109,7 +106,7 @@ function isThemeDirectory(projectPath) {
109
106
  }
110
107
 
111
108
  // src/wp-playground-wordpress/is-wp-content-directory.ts
112
- import fs5 from "fs-extra";
109
+ import fs5 from "fs";
113
110
  import path4 from "path";
114
111
  function isWpContentDirectory(projectPath) {
115
112
  const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
@@ -122,14 +119,14 @@ function isWpContentDirectory(projectPath) {
122
119
  }
123
120
 
124
121
  // src/wp-playground-wordpress/is-wordpress-directory.ts
125
- import fs6 from "fs-extra";
122
+ import fs6 from "fs";
126
123
  import path5 from "path";
127
124
  function isWordPressDirectory(projectPath) {
128
125
  return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
129
126
  }
130
127
 
131
128
  // src/wp-playground-wordpress/is-wordpress-develop-directory.ts
132
- import fs7 from "fs-extra";
129
+ import fs7 from "fs";
133
130
  import path6 from "path";
134
131
  function isWordPressDevelopDirectory(projectPath) {
135
132
  const requiredFiles = [
@@ -153,40 +150,29 @@ function shouldOutput() {
153
150
  }
154
151
  var output = shouldOutput() ? console : null;
155
152
 
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";
153
+ // src/paths.ts
154
+ import crypto from "crypto";
155
+ import fs8 from "fs";
162
156
  import os from "os";
163
- function getWpNowTmpPath() {
164
- const tmpDirectory = os.tmpdir();
165
- return path7.join(tmpDirectory, `wp-now-tests-hidden-folder`);
157
+ import path7 from "path";
158
+ function getWpAppHome() {
159
+ return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
166
160
  }
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();
161
+ function getCachePath() {
162
+ return ensureDir(path7.join(getWpAppHome(), "cache"));
174
163
  }
175
-
176
- // src/get-wordpress-versions-path.ts
177
- import path9 from "path";
178
- function getWordpressVersionsPath() {
179
- return path9.join(getWpNowPath(), "wordpress-versions");
164
+ 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}`);
180
168
  }
181
-
182
- // src/get-sqlite-path.ts
183
- import path10 from "path";
184
- function getSqlitePath() {
185
- return path10.join(getWpNowPath(), `${SQLITE_FILENAME}-main`);
169
+ function ensureDir(dir) {
170
+ fs8.mkdirSync(dir, { recursive: true });
171
+ return dir;
186
172
  }
187
173
 
188
174
  // src/download.ts
189
- function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) {
175
+ function getWordPressVersionUrl(version) {
190
176
  if (!isValidWordPressVersion(version)) {
191
177
  throw new Error(
192
178
  '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 +187,172 @@ function getClassicPressVersionUrl(version) {
201
187
  const tag = version.slice("classicpress-".length);
202
188
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
203
189
  }
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) {
190
+ async function cachedZip(url, cacheKey) {
191
+ const cacheFile = path8.join(getCachePath(), cacheKey);
192
+ if (!fs9.existsSync(cacheFile)) {
193
+ output?.log(`Downloading ${cacheKey}...`);
194
+ const response = await fetch(url, { redirect: "follow" });
195
+ if (!response.ok) {
225
196
  throw new Error(
226
- `Failed to download file (Status code ${response.statusCode}).`
197
+ `Failed to download ${url} (HTTP ${response.status}).`
227
198
  );
228
199
  }
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);
200
+ const bytes = Buffer.from(await response.arrayBuffer());
201
+ fs9.writeFileSync(`${cacheFile}.partial`, bytes);
202
+ fs9.renameSync(`${cacheFile}.partial`, cacheFile);
203
+ output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
264
204
  }
205
+ return new File([fs9.readFileSync(cacheFile)], cacheKey);
265
206
  }
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);
207
+ async function getCoreZip(version) {
208
+ if (isClassicPressVersion(version)) {
209
+ return cachedZip(getClassicPressVersionUrl(version), `${version}.zip`);
287
210
  }
211
+ return cachedZip(getWordPressVersionUrl(version), `wordpress-${version}.zip`);
288
212
  }
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
- );
213
+ async function getSqliteIntegrationZip() {
214
+ return cachedZip(SQLITE_URL, "sqlite-database-integration.zip");
312
215
  }
313
216
 
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
- });
217
+ // src/engine.ts
218
+ async function startWPApp(options) {
219
+ const projectPath = path9.resolve(options.projectPath);
220
+ const mode = options.mode;
361
221
  output?.log(`directory: ${options.projectPath}`);
362
- output?.log(`mode: ${options.mode}`);
222
+ output?.log(`mode: ${mode}`);
363
223
  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 };
369
- }
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;
397
- }
398
- });
399
- await installationStep2(php);
400
- await login(php, {
401
- username: "admin",
402
- password: "password"
224
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
225
+ const isWordPressBacked = mode !== "index" /* INDEX */;
226
+ const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
227
+ if (isWordPressBacked) {
228
+ 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"
403
254
  });
404
- if (isFirstTimeProject && ["plugin" /* PLUGIN */, "theme" /* THEME */].includes(options.mode)) {
405
- await activatePluginOrTheme(php, options);
255
+ const php = await requestHandler.getPrimaryPhp();
256
+ await applyProjectMounts(php, mode, projectPath);
257
+ if (freshSite) {
258
+ await activateProject(php, mode, projectPath);
259
+ }
260
+ return { requestHandler, php, options, freshSite };
261
+ }
262
+ async function applyProjectMounts(php, mode, projectPath) {
263
+ const projectName = path9.basename(projectPath);
264
+ switch (mode) {
265
+ case "theme" /* THEME */:
266
+ await mountAt(
267
+ php,
268
+ projectPath,
269
+ `${DOCROOT}/wp-content/themes/${projectName}`
270
+ );
271
+ break;
272
+ case "plugin" /* PLUGIN */:
273
+ await mountAt(
274
+ php,
275
+ projectPath,
276
+ `${DOCROOT}/wp-content/plugins/${projectName}`
277
+ );
278
+ break;
279
+ case "wp-content" /* WP_CONTENT */:
280
+ for (const entry of fs10.readdirSync(projectPath)) {
281
+ if (entry === "index.php") {
282
+ continue;
283
+ }
284
+ await mountAt(
285
+ php,
286
+ path9.join(projectPath, entry),
287
+ `${DOCROOT}/wp-content/${entry}`
288
+ );
289
+ }
290
+ break;
291
+ default:
292
+ break;
406
293
  }
407
- return {
408
- php,
409
- phpInstances,
410
- options
411
- };
412
294
  }
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
- });
441
- }
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);
295
+ async function mountAt(php, hostPath, vfsPath) {
296
+ if (fs10.statSync(hostPath).isDirectory()) {
297
+ php.mkdir(vfsPath);
298
+ }
299
+ await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
484
300
  }
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`)
301
+ async function activateProject(php, mode, projectPath) {
302
+ const projectName = path9.basename(projectPath);
303
+ if (mode === "theme" /* THEME */) {
304
+ await runWordPressCode(
305
+ php,
306
+ `switch_theme(${phpString(projectName)});`
307
+ );
308
+ } else if (mode === "plugin" /* PLUGIN */) {
309
+ const pluginFile = findPluginFile(projectPath);
310
+ if (pluginFile) {
311
+ await runWordPressCode(
312
+ php,
313
+ `activate_plugin(${phpString(`${projectName}/${pluginFile}`)});`
314
+ );
315
+ }
316
+ } else if (mode === "wp-content" /* WP_CONTENT */) {
317
+ await runWordPressCode(
318
+ php,
319
+ `$theme = wp_get_theme();
320
+ if (!$theme->exists()) {
321
+ $themes = wp_get_themes();
322
+ if (count($themes) > 0) {
323
+ switch_theme(array_keys($themes)[0]);
324
+ }
325
+ }`
507
326
  );
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
327
  }
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
328
  }
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`
546
- );
329
+ async function runWordPressCode(php, code) {
330
+ const response = await php.run({
331
+ code: `<?php
332
+ require ${phpString(`${DOCROOT}/wp-load.php`)};
333
+ require_once ${phpString(`${DOCROOT}/wp-admin/includes/plugin.php`)};
334
+ ${code}
335
+ `
336
+ });
337
+ if (response.exitCode !== 0) {
338
+ output?.error(`Activation step failed: ${response.errors}`);
547
339
  }
548
340
  }
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
- );
341
+ function phpString(value) {
342
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
343
+ }
344
+ function findPluginFile(projectPath) {
345
+ const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
346
+ for (const file of fs10.readdirSync(projectPath)) {
347
+ if (!file.endsWith(".php")) {
348
+ continue;
349
+ }
350
+ const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
351
+ if (pluginNameRegex.test(content)) {
352
+ return file;
353
+ }
354
+ }
355
+ return null;
555
356
  }
556
357
  function inferMode(projectPath) {
557
358
  if (isWordPressDevelopDirectory(projectPath)) {
@@ -569,27 +370,6 @@ function inferMode(projectPath) {
569
370
  }
570
371
  return "playground" /* PLAYGROUND */;
571
372
  }
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: fields.toString()
591
- });
592
- }
593
373
 
594
374
  // src/port-finder.ts
595
375
  import http from "http";
@@ -641,14 +421,11 @@ var PortFinder = class _PortFinder {
641
421
  var portFinder = PortFinder.getInstance();
642
422
 
643
423
  // src/config.ts
644
- import path13 from "path";
645
424
  var DEFAULT_OPTIONS = {
646
425
  phpVersion: DEFAULT_PHP_VERSION,
647
426
  wordPressVersion: DEFAULT_WORDPRESS_VERSION,
648
- documentRoot: "/var/www/html",
649
427
  projectPath: process.cwd(),
650
- mode: "auto" /* AUTO */,
651
- numberOfPhpInstances: 1
428
+ mode: "auto" /* AUTO */
652
429
  };
653
430
  async function getAbsoluteURL() {
654
431
  const port = await portFinder.getOpenPort();
@@ -657,13 +434,7 @@ async function getAbsoluteURL() {
657
434
  }
658
435
  return `http://localhost:${port}`;
659
436
  }
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) {
437
+ async function getWpAppConfig(args) {
667
438
  if (args.port) {
668
439
  portFinder.setPort(args.port);
669
440
  }
@@ -686,12 +457,6 @@ async function getWpNowConfig(args) {
686
457
  if (!options.mode || options.mode === "auto") {
687
458
  options.mode = inferMode(options.projectPath);
688
459
  }
689
- if (!options.wpContentPath) {
690
- options.wpContentPath = getWpContentHomePath(
691
- options.projectPath,
692
- options.mode
693
- );
694
- }
695
460
  if (!options.absoluteUrl) {
696
461
  options.absoluteUrl = await getAbsoluteURL();
697
462
  }
@@ -709,94 +474,83 @@ async function getWpNowConfig(args) {
709
474
  }
710
475
 
711
476
  // src/start-server.ts
712
- import fs10 from "fs";
477
+ import fs11 from "fs";
478
+ import { Readable } from "stream";
479
+ import { pipeline } from "stream/promises";
713
480
  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
481
  async function startServer(options = {}) {
736
- if (!fs10.existsSync(options.projectPath)) {
482
+ if (!fs11.existsSync(options.projectPath)) {
737
483
  throw new Error(
738
484
  `The given path "${options.projectPath}" does not exist.`
739
485
  );
740
486
  }
741
- const app = express();
742
- app.use(fileUpload());
743
487
  const port = await portFinder.getOpenPort();
744
- const { php, options: wpNowOptions } = await startWPNow(options);
488
+ const site = await startWPApp(options);
489
+ const app = express();
745
490
  app.use("/", async (req, res) => {
746
491
  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 = {
492
+ const phpRequest = {
760
493
  url: req.url,
761
- headers: requestHeaders,
762
494
  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
495
+ headers: parseHeaders(req),
496
+ body: await bufferRequestBody(req)
778
497
  };
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);
498
+ const response = await site.requestHandler.requestStreamed(phpRequest);
499
+ await sendStreamedResponse(response, res);
500
+ } catch (error) {
501
+ output?.trace(error);
502
+ if (!res.headersSent) {
503
+ res.statusCode = 500;
504
+ res.end("Internal Server Error");
505
+ }
787
506
  }
788
507
  });
789
508
  const url = options.absoluteUrl;
790
509
  app.listen(port, () => {
791
510
  output?.log(`Server running at ${url}`);
792
511
  });
793
- return {
794
- url,
795
- php,
796
- options: wpNowOptions
797
- };
512
+ return { ...site, url };
513
+ }
514
+ async function sendStreamedResponse(streamedResponse, res) {
515
+ const [headers, httpStatusCode] = await Promise.all([
516
+ streamedResponse.headers,
517
+ streamedResponse.httpStatusCode
518
+ ]);
519
+ res.statusCode = httpStatusCode;
520
+ for (const key in headers) {
521
+ res.setHeader(key, headers[key]);
522
+ }
523
+ const nodeStream = Readable.fromWeb(streamedResponse.stdout);
524
+ try {
525
+ await pipeline(nodeStream, res);
526
+ } catch (error) {
527
+ if (error instanceof Error && "code" in error && (error.code === "ERR_STREAM_PREMATURE_CLOSE" || error.code === "ERR_STREAM_UNABLE_TO_PIPE")) {
528
+ return;
529
+ }
530
+ throw error;
531
+ }
798
532
  }
533
+ var bufferRequestBody = async (req) => await new Promise((resolve) => {
534
+ const body = [];
535
+ req.on("data", (chunk) => {
536
+ body.push(chunk);
537
+ });
538
+ req.on("end", () => {
539
+ resolve(new Uint8Array(Buffer.concat(body)));
540
+ });
541
+ });
542
+ var parseHeaders = (req) => {
543
+ const requestHeaders = {};
544
+ if (req.rawHeaders && req.rawHeaders.length) {
545
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
546
+ requestHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1];
547
+ }
548
+ }
549
+ return requestHeaders;
550
+ };
799
551
  export {
800
- getWpNowConfig,
801
- startServer
552
+ getWpAppConfig,
553
+ inferMode,
554
+ startServer,
555
+ startWPApp
802
556
  };