pressship 0.1.12 → 0.1.14

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 (57) hide show
  1. package/README.md +13 -0
  2. package/assets/web/app.js +7322 -0
  3. package/assets/web/harness-sdk-icon-mono.svg +33 -0
  4. package/assets/web/harness-sdk-icon.svg +120 -0
  5. package/assets/web/index.html +392 -0
  6. package/assets/web/style.css +7906 -0
  7. package/dist/cli.js +14 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/plugin/demo.d.ts +75 -4
  10. package/dist/plugin/demo.js +520 -31
  11. package/dist/plugin/demo.js.map +1 -1
  12. package/dist/utils/paths.d.ts +5 -0
  13. package/dist/utils/paths.js +15 -0
  14. package/dist/utils/paths.js.map +1 -1
  15. package/dist/web/ai-assistance.d.ts +70 -0
  16. package/dist/web/ai-assistance.js +262 -0
  17. package/dist/web/ai-assistance.js.map +1 -0
  18. package/dist/web/index.d.ts +9 -0
  19. package/dist/web/index.js +34 -0
  20. package/dist/web/index.js.map +1 -0
  21. package/dist/web/jobs.d.ts +33 -0
  22. package/dist/web/jobs.js +155 -0
  23. package/dist/web/jobs.js.map +1 -0
  24. package/dist/web/open-url.d.ts +1 -0
  25. package/dist/web/open-url.js +13 -0
  26. package/dist/web/open-url.js.map +1 -0
  27. package/dist/web/plugin-check-state.d.ts +47 -0
  28. package/dist/web/plugin-check-state.js +107 -0
  29. package/dist/web/plugin-check-state.js.map +1 -0
  30. package/dist/web/plugin-check.d.ts +11 -0
  31. package/dist/web/plugin-check.js +124 -0
  32. package/dist/web/plugin-check.js.map +1 -0
  33. package/dist/web/ports.d.ts +2 -0
  34. package/dist/web/ports.js +38 -0
  35. package/dist/web/ports.js.map +1 -0
  36. package/dist/web/registry.d.ts +49 -0
  37. package/dist/web/registry.js +106 -0
  38. package/dist/web/registry.js.map +1 -0
  39. package/dist/web/release.d.ts +87 -0
  40. package/dist/web/release.js +417 -0
  41. package/dist/web/release.js.map +1 -0
  42. package/dist/web/server.d.ts +31 -0
  43. package/dist/web/server.js +1453 -0
  44. package/dist/web/server.js.map +1 -0
  45. package/dist/web/settings.d.ts +36 -0
  46. package/dist/web/settings.js +72 -0
  47. package/dist/web/settings.js.map +1 -0
  48. package/dist/web/version-state.d.ts +28 -0
  49. package/dist/web/version-state.js +118 -0
  50. package/dist/web/version-state.js.map +1 -0
  51. package/dist/wordpress-org/publish.d.ts +1 -0
  52. package/dist/wordpress-org/publish.js +4 -2
  53. package/dist/wordpress-org/publish.js.map +1 -1
  54. package/dist/wordpress-org/submit.d.ts +1 -0
  55. package/dist/wordpress-org/submit.js +7 -5
  56. package/dist/wordpress-org/submit.js.map +1 -1
  57. package/package.json +4 -1
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { createServer as createNetServer } from "node:net";
3
4
  import { homedir } from "node:os";
4
5
  import path from "node:path";
5
6
  import { execa } from "execa";
@@ -10,23 +11,106 @@ import { ui } from "../ui.js";
10
11
  import { ensureCacheDir, pathExists } from "../utils/paths.js";
11
12
  const playgroundCliPackage = "@wp-playground/cli@latest";
12
13
  const compatibilityMuPluginPath = "/wordpress/wp-content/mu-plugins/pressship-playground-compat.php";
14
+ const defaultMysqlDatabasePrefix = "pressship_playground";
15
+ const managedMysqlContainerName = "pressship-playground-mariadb";
16
+ const managedMysqlImage = "mariadb:10.6";
17
+ const managedMysqlPassword = "pressship";
13
18
  const compatibilityMuPlugin = `<?php
14
- error_reporting(error_reporting() & ~E_DEPRECATED & ~E_USER_DEPRECATED);
15
- set_error_handler(static function (int $severity): bool {
16
- return $severity === E_DEPRECATED || $severity === E_USER_DEPRECATED;
17
- });
19
+ $pressship_deprecated_mask = (defined('E_DEPRECATED') ? E_DEPRECATED : 0) | (defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : 0);
20
+ if ($pressship_deprecated_mask) {
21
+ error_reporting(error_reporting() & ~$pressship_deprecated_mask);
22
+ }
23
+
24
+ function pressship_silence_deprecated_errors($severity) {
25
+ $deprecated_mask = (defined('E_DEPRECATED') ? E_DEPRECATED : 0) | (defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : 0);
26
+ return $deprecated_mask && (($severity & $deprecated_mask) !== 0);
27
+ }
28
+
29
+ set_error_handler('pressship_silence_deprecated_errors');
18
30
 
19
31
  if (! function_exists('is_plugin_active')) {
20
32
  require_once ABSPATH . 'wp-admin/includes/plugin.php';
21
33
  }
34
+
35
+ function pressship_remove_frame_options_header() {
36
+ remove_action('admin_init', 'send_frame_options_header');
37
+ remove_action('login_init', 'send_frame_options_header');
38
+ if (function_exists('header_remove')) {
39
+ header_remove('X-Frame-Options');
40
+ }
41
+ }
42
+
43
+ pressship_remove_frame_options_header();
44
+
45
+ function pressship_filter_playground_headers($headers) {
46
+ unset($headers['X-Frame-Options']);
47
+ unset($headers['x-frame-options']);
48
+ return $headers;
49
+ }
50
+
51
+ add_filter('wp_headers', 'pressship_filter_playground_headers');
52
+
53
+ add_action('init', 'pressship_remove_frame_options_header', 0);
54
+ add_action('admin_init', 'pressship_remove_frame_options_header', 0);
55
+ add_action('admin_init', 'pressship_remove_frame_options_header', PHP_INT_MAX);
56
+ add_action('login_init', 'pressship_remove_frame_options_header', 0);
57
+ add_action('login_init', 'pressship_remove_frame_options_header', PHP_INT_MAX);
58
+ add_action('send_headers', 'pressship_remove_frame_options_header', PHP_INT_MAX);
59
+
60
+ function pressship_ensure_playground_admin() {
61
+ if (! username_exists('admin')) {
62
+ $user_id = wp_create_user('admin', 'password', 'admin@example.test');
63
+ if (! is_wp_error($user_id)) {
64
+ $user = new WP_User($user_id);
65
+ $user->set_role('administrator');
66
+ }
67
+ }
68
+
69
+ if (! is_admin() || is_user_logged_in() || ! isset($_GET['pressship_auto_login'])) {
70
+ return;
71
+ }
72
+
73
+ $user = get_user_by('login', 'admin');
74
+ if (! $user) {
75
+ return;
76
+ }
77
+
78
+ wp_set_current_user($user->ID);
79
+ wp_set_auth_cookie($user->ID);
80
+ wp_safe_redirect(remove_query_arg('pressship_auto_login'));
81
+ exit;
82
+ }
83
+
84
+ add_action('init', 'pressship_ensure_playground_admin');
22
85
  `;
86
+ const playgroundDatabaseModeSchema = z.enum(["auto", "sqlite", "mysql"]);
23
87
  const demoOptionsSchema = z.object({
24
88
  port: z.string().optional(),
25
89
  wp: z.string().optional(),
26
90
  php: z.string().optional(),
91
+ database: playgroundDatabaseModeSchema.default("auto"),
92
+ mysqlHost: z.string().trim().min(1).default("127.0.0.1"),
93
+ mysqlPort: z.coerce.number().int().min(1).max(65535).default(3306),
94
+ mysqlUser: z.string().trim().min(1).default("root"),
95
+ mysqlPassword: z.string().default(""),
96
+ mysqlDatabasePrefix: z
97
+ .string()
98
+ .trim()
99
+ .regex(/^[A-Za-z0-9_]+$/, "MySQL database prefix can only contain letters, numbers, and underscores.")
100
+ .min(1)
101
+ .max(40)
102
+ .default(defaultMysqlDatabasePrefix),
27
103
  reset: z.boolean().default(false),
28
104
  skipBrowser: z.boolean().default(false)
29
105
  });
106
+ export class PlaygroundRuntimeUnsupportedError extends Error {
107
+ details;
108
+ constructor(message, details) {
109
+ super(message);
110
+ this.details = details;
111
+ this.name = "PlaygroundRuntimeUnsupportedError";
112
+ }
113
+ }
30
114
  export async function demo(target, rawOptions = {}) {
31
115
  const options = demoOptionsSchema.parse(rawOptions);
32
116
  ui.intro("Start plugin demo");
@@ -37,6 +121,7 @@ export async function demo(target, rawOptions = {}) {
37
121
  ui.keyValue("Slug", plan.slug);
38
122
  ui.keyValue("WordPress", plan.wpVersion ?? "default");
39
123
  ui.keyValue("PHP", plan.phpVersion ?? "default");
124
+ ui.keyValue("Database", describePlaygroundDatabase(plan.database));
40
125
  ui.keyValue("Site", ui.path(plan.siteDir));
41
126
  if (plan.url) {
42
127
  ui.keyValue("URL", ui.path(plan.url));
@@ -45,36 +130,58 @@ export async function demo(target, rawOptions = {}) {
45
130
  if (options.reset) {
46
131
  await ui.task("Resetting persisted Playground site", () => resetPlaygroundSite(plan.siteDir), () => "Reset complete");
47
132
  }
133
+ assertDemoLaunchPlanSupported(plan);
134
+ await prepareDemoRuntime(plan, { resetDatabase: options.reset });
48
135
  ui.info("Starting WordPress Playground. Press Ctrl+C to stop the local server.");
49
136
  await execa(plan.command, plan.args, {
50
137
  cwd: plan.cwd,
51
138
  stdio: "inherit"
52
139
  });
53
140
  }
54
- export async function createDemoLaunchPlan(target, options) {
141
+ export async function createDemoLaunchPlan(target, rawOptions) {
142
+ const options = demoOptionsSchema.parse(rawOptions);
55
143
  const resolvedTarget = target ?? process.cwd();
56
144
  if (isLocalPluginTarget(resolvedTarget)) {
57
145
  const project = await discoverPluginProject(resolvedTarget);
58
- const blueprintPath = await writeDemoBlueprint(project.slug, createLocalDemoBlueprint(project));
59
146
  const playgroundOptions = resolveDemoPlaygroundOptions(options, {
60
147
  wp: project.headers.requiresAtLeast ?? project.readme?.requiresAtLeast,
61
148
  php: project.headers.requiresPhp ?? project.readme?.requiresPhp
62
149
  });
150
+ const databaseMode = resolvePlaygroundDatabaseMode(playgroundOptions);
151
+ const playgroundCwd = await createPlaygroundRuntimeCwd(project.rootDir, project.slug, playgroundOptions, databaseMode);
152
+ const blueprintPath = await writeDemoBlueprint(project.slug, createLocalDemoBlueprint(project));
63
153
  const url = options.port ? `http://127.0.0.1:${options.port}` : undefined;
64
- return {
65
- source: "local",
66
- name: project.headers.pluginName,
154
+ const database = await createPlaygroundDatabasePlan({
155
+ mode: databaseMode,
156
+ runtimeCwd: playgroundCwd,
157
+ siteDir: getPlaygroundSiteDirForRuntime(playgroundCwd, databaseMode),
67
158
  slug: project.slug,
68
- command: "npx",
69
- args: createPlaygroundStartArgs({
159
+ sourcePath: project.rootDir,
160
+ options: playgroundOptions
161
+ });
162
+ const args = database.mode === "mysql"
163
+ ? createPlaygroundServerArgs({
164
+ blueprintPath,
165
+ mount: `${project.rootDir}:/wordpress/wp-content/plugins/${project.slug}`,
166
+ database,
167
+ options: playgroundOptions
168
+ })
169
+ : createPlaygroundStartArgs({
70
170
  path: project.rootDir,
71
171
  blueprintPath,
72
172
  mount: `${project.rootDir}:/wordpress/wp-content/plugins/${project.slug}`,
73
173
  options: playgroundOptions
74
- }),
75
- cwd: project.rootDir,
174
+ });
175
+ return {
176
+ source: "local",
177
+ name: project.headers.pluginName,
178
+ slug: project.slug,
179
+ command: "npx",
180
+ args,
181
+ cwd: playgroundCwd,
76
182
  blueprintPath,
77
- siteDir: getPlaygroundSiteDir(project.rootDir),
183
+ siteDir: getPlaygroundSiteDirForRuntime(playgroundCwd, databaseMode),
184
+ database,
78
185
  wpVersion: playgroundOptions.wp,
79
186
  phpVersion: playgroundOptions.php,
80
187
  url
@@ -82,26 +189,44 @@ export async function createDemoLaunchPlan(target, options) {
82
189
  }
83
190
  const slug = slugFromHostedTarget(resolvedTarget);
84
191
  const plugin = await fetchHostedPluginInfo(slug);
85
- const blueprintPath = await writeDemoBlueprint(slug, createHostedDemoBlueprint(slug));
86
- const demoDir = path.dirname(blueprintPath);
87
192
  const playgroundOptions = resolveDemoPlaygroundOptions(options, {
88
193
  wp: plugin.requires,
89
194
  php: plugin.requiresPhp
90
195
  });
196
+ const blueprintPath = await writeDemoBlueprint(slug, createHostedDemoBlueprint(slug));
197
+ const demoDir = path.dirname(blueprintPath);
198
+ const databaseMode = resolvePlaygroundDatabaseMode(playgroundOptions);
199
+ const playgroundCwd = await createPlaygroundRuntimeCwd(demoDir, slug, playgroundOptions, databaseMode);
91
200
  const url = options.port ? `http://127.0.0.1:${options.port}` : undefined;
201
+ const database = await createPlaygroundDatabasePlan({
202
+ mode: databaseMode,
203
+ runtimeCwd: playgroundCwd,
204
+ siteDir: getPlaygroundSiteDirForRuntime(playgroundCwd, databaseMode),
205
+ slug,
206
+ sourcePath: demoDir,
207
+ options: playgroundOptions
208
+ });
209
+ const args = database.mode === "mysql"
210
+ ? createPlaygroundServerArgs({
211
+ blueprintPath,
212
+ database,
213
+ options: playgroundOptions
214
+ })
215
+ : createPlaygroundStartArgs({
216
+ path: demoDir,
217
+ blueprintPath,
218
+ options: playgroundOptions
219
+ });
92
220
  return {
93
221
  source: "wordpress.org",
94
222
  name: plugin.name,
95
223
  slug,
96
224
  command: "npx",
97
- args: createPlaygroundStartArgs({
98
- path: demoDir,
99
- blueprintPath,
100
- options: playgroundOptions
101
- }),
102
- cwd: demoDir,
225
+ args,
226
+ cwd: playgroundCwd,
103
227
  blueprintPath,
104
- siteDir: getPlaygroundSiteDir(demoDir),
228
+ siteDir: getPlaygroundSiteDirForRuntime(playgroundCwd, databaseMode),
229
+ database,
105
230
  wpVersion: playgroundOptions.wp,
106
231
  phpVersion: playgroundOptions.php,
107
232
  url
@@ -153,6 +278,7 @@ export function createPlaygroundCompatibilitySteps() {
153
278
  ];
154
279
  }
155
280
  export function createPlaygroundStartArgs(input) {
281
+ const options = demoOptionsSchema.parse(input.options);
156
282
  return [
157
283
  "--yes",
158
284
  playgroundCliPackage,
@@ -166,24 +292,120 @@ export function createPlaygroundStartArgs(input) {
166
292
  "WP_DEBUG_DISPLAY",
167
293
  "false",
168
294
  ...(input.mount ? ["--no-auto-mount", `--mount=${input.mount}`] : []),
169
- ...(input.options.port ? [`--port=${input.options.port}`] : []),
170
- ...(input.options.wp ? [`--wp=${input.options.wp}`] : []),
171
- ...(input.options.php ? [`--php=${input.options.php}`] : []),
172
- ...(input.options.skipBrowser ? ["--skip-browser"] : [])
295
+ ...(options.port ? [`--port=${options.port}`] : []),
296
+ ...(options.wp ? [`--wp=${options.wp}`] : []),
297
+ ...(options.php ? [`--php=${options.php}`] : []),
298
+ ...(options.skipBrowser ? ["--skip-browser"] : [])
173
299
  ];
174
300
  }
175
- export function resolveDemoPlaygroundOptions(options, requiredVersions) {
301
+ export function createPlaygroundServerArgs(input) {
302
+ const options = demoOptionsSchema.parse(input.options);
303
+ const siteUrl = options.port ? `http://127.0.0.1:${options.port}` : undefined;
304
+ return [
305
+ "--yes",
306
+ playgroundCliPackage,
307
+ "server",
308
+ `--mount-before-install=${input.database.configPath}:/wordpress/wp-config.php`,
309
+ `--mount-before-install=${input.database.sqliteBypassDir}:/wordpress/wp-content/mu-plugins/sqlite-database-integration`,
310
+ "--skip-sqlite-setup",
311
+ `--blueprint=${input.blueprintPath}`,
312
+ ...(input.mount ? [`--mount=${input.mount}`] : []),
313
+ ...(options.port ? [`--port=${options.port}`] : []),
314
+ ...(siteUrl ? [`--site-url=${siteUrl}`] : []),
315
+ ...(options.wp ? [`--wp=${options.wp}`] : []),
316
+ ...(options.php ? [`--php=${options.php}`] : [])
317
+ ];
318
+ }
319
+ export function resolveDemoPlaygroundOptions(rawOptions, _requiredVersions) {
320
+ const options = demoOptionsSchema.parse(rawOptions);
321
+ const wp = normalizePlaygroundVersion(options.wp);
322
+ const explicitPhp = normalizePlaygroundVersion(options.php);
176
323
  return {
177
324
  ...options,
178
- wp: options.wp ?? normalizePlaygroundVersion(requiredVersions.wp),
179
- php: options.php ?? normalizePlaygroundVersion(requiredVersions.php)
325
+ wp,
326
+ php: explicitPhp ?? inferPlaygroundPhpVersionForWordPress(wp)
180
327
  };
181
328
  }
329
+ export function resolvePlaygroundDatabaseMode(options) {
330
+ if (options.database === "mysql") {
331
+ return "mysql";
332
+ }
333
+ if (options.database === "sqlite") {
334
+ return "sqlite";
335
+ }
336
+ const branch = parseWordPressBranch(options.wp);
337
+ return branch && !isWordPressBranchAtLeast(branch, 4, 7) ? "mysql" : "sqlite";
338
+ }
339
+ export async function prepareDemoRuntime(plan, options = {}) {
340
+ if (plan.database.mode !== "mysql") {
341
+ return;
342
+ }
343
+ await mkdir(plan.siteDir, { recursive: true, mode: 0o700 });
344
+ await mkdir(plan.database.sqliteBypassDir, { recursive: true, mode: 0o700 });
345
+ await writeFile(plan.database.configPath, createMysqlWpConfig(plan.database), { mode: 0o600 });
346
+ await prepareMysqlDatabase(plan.database, { reset: options.resetDatabase ?? false });
347
+ }
348
+ export function publicDemoLaunchPlan(plan) {
349
+ if (plan.database.mode === "sqlite") {
350
+ return { ...plan, database: { mode: "sqlite" } };
351
+ }
352
+ const { host, port, user, database, server } = plan.database;
353
+ return {
354
+ ...plan,
355
+ database: {
356
+ mode: "mysql",
357
+ host,
358
+ port,
359
+ user,
360
+ database,
361
+ server
362
+ }
363
+ };
364
+ }
365
+ export function inferPlaygroundPhpVersionForWordPress(wpVersion) {
366
+ const branch = parseWordPressBranch(wpVersion);
367
+ if (!branch) {
368
+ return undefined;
369
+ }
370
+ if (isWordPressBranchAtLeast(branch, 6, 9))
371
+ return "8.5";
372
+ if (isWordPressBranchAtLeast(branch, 6, 7))
373
+ return "8.4";
374
+ if (isWordPressBranchAtLeast(branch, 6, 4))
375
+ return "8.3";
376
+ if (isWordPressBranchAtLeast(branch, 6, 1))
377
+ return "8.2";
378
+ if (isWordPressBranchAtLeast(branch, 5, 9))
379
+ return "8.1";
380
+ if (isWordPressBranchAtLeast(branch, 5, 6))
381
+ return "8.0";
382
+ if (isWordPressBranchAtLeast(branch, 4, 7))
383
+ return "7.4";
384
+ return "5.2";
385
+ }
386
+ export function getPlaygroundRuntimeLimitation(wpVersion) {
387
+ const branch = parseWordPressBranch(wpVersion);
388
+ if (!branch || isWordPressBranchAtLeast(branch, 4, 7)) {
389
+ return undefined;
390
+ }
391
+ return (`WordPress Playground cannot currently run WordPress ${wpVersion} with its matching legacy PHP runtime. ` +
392
+ "The PHP 5.2 runtime cannot parse the current SQLite integration, and the MySQL path needs the old PHP mysql extension that Playground does not provide. " +
393
+ "Use Latest or WordPress 4.7+ in Playground, or test this version in a local PHP/MySQL environment outside Playground.");
394
+ }
395
+ export function assertDemoLaunchPlanSupported(plan) {
396
+ const limitation = getPlaygroundRuntimeLimitation(plan.wpVersion);
397
+ if (limitation) {
398
+ throw new PlaygroundRuntimeUnsupportedError(limitation, {
399
+ wpVersion: plan.wpVersion,
400
+ phpVersion: plan.phpVersion
401
+ });
402
+ }
403
+ }
182
404
  export function getPlaygroundSiteDir(projectPath) {
183
405
  const siteId = createHash("sha256").update(projectPath).digest("hex");
184
406
  return path.join(homedir(), ".wordpress-playground", "sites", siteId);
185
407
  }
186
- async function resetPlaygroundSite(siteDir) {
408
+ export async function resetPlaygroundSite(siteDir) {
187
409
  await rm(siteDir, { recursive: true, force: true });
188
410
  }
189
411
  async function writeDemoBlueprint(slug, blueprint) {
@@ -194,6 +416,260 @@ async function writeDemoBlueprint(slug, blueprint) {
194
416
  await writeFile(blueprintPath, `${JSON.stringify(blueprint, null, 2)}\n`);
195
417
  return blueprintPath;
196
418
  }
419
+ async function createPlaygroundRuntimeCwd(sourcePath, slug, options, databaseMode) {
420
+ const cacheDir = await ensureCacheDir();
421
+ const runtimeId = createHash("sha256")
422
+ .update(`${sourcePath}\0wp=${options.wp ?? "latest"}\0php=${options.php ?? "latest"}\0db=${databaseMode}`)
423
+ .digest("hex");
424
+ const runtimeCwd = path.join(cacheDir, "playground-runtime", slug, runtimeId);
425
+ await mkdir(runtimeCwd, { recursive: true, mode: 0o700 });
426
+ return runtimeCwd;
427
+ }
428
+ function getPlaygroundSiteDirForRuntime(runtimeCwd, databaseMode) {
429
+ return databaseMode === "mysql" ? path.join(runtimeCwd, "wordpress") : getPlaygroundSiteDir(runtimeCwd);
430
+ }
431
+ async function createPlaygroundDatabasePlan(input) {
432
+ if (input.mode === "sqlite") {
433
+ return { mode: "sqlite" };
434
+ }
435
+ const database = createPlaygroundMysqlDatabaseName(input.options.mysqlDatabasePrefix, input.slug, `${input.sourcePath}\0wp=${input.options.wp ?? "latest"}\0php=${input.options.php ?? "latest"}`);
436
+ const configPath = path.join(input.siteDir, "wp-config.mysql.php");
437
+ const sqliteBypassDir = path.join(input.siteDir, "sqlite-database-integration");
438
+ const plan = {
439
+ mode: "mysql",
440
+ host: input.options.mysqlHost,
441
+ port: input.options.mysqlPort,
442
+ user: input.options.mysqlUser,
443
+ password: input.options.mysqlPassword,
444
+ database,
445
+ configPath,
446
+ sqliteBypassDir,
447
+ server: "external"
448
+ };
449
+ return plan;
450
+ }
451
+ function createPlaygroundMysqlDatabaseName(prefix, slug, key) {
452
+ const safePrefix = sanitizeMysqlIdentifier(prefix) || defaultMysqlDatabasePrefix;
453
+ const safeSlug = sanitizeMysqlIdentifier(slug) || "plugin";
454
+ const hash = createHash("sha256").update(key).digest("hex").slice(0, 10);
455
+ const maxSlugLength = Math.max(1, 64 - safePrefix.length - hash.length - 2);
456
+ return `${safePrefix}_${safeSlug.slice(0, maxSlugLength)}_${hash}`.slice(0, 64);
457
+ }
458
+ function sanitizeMysqlIdentifier(value) {
459
+ return value.replace(/[^A-Za-z0-9_]/g, "_").replace(/^_+|_+$/g, "");
460
+ }
461
+ function createMysqlWpConfig(database) {
462
+ const dbHost = database.port === 3306 ? database.host : `${database.host}:${database.port}`;
463
+ const salts = [
464
+ "AUTH_KEY",
465
+ "SECURE_AUTH_KEY",
466
+ "LOGGED_IN_KEY",
467
+ "NONCE_KEY",
468
+ "AUTH_SALT",
469
+ "SECURE_AUTH_SALT",
470
+ "LOGGED_IN_SALT",
471
+ "NONCE_SALT"
472
+ ]
473
+ .map((name) => `define(${phpString(name)}, ${phpString(randomSalt())});`)
474
+ .join("\n");
475
+ return `<?php
476
+ define('DB_NAME', ${phpString(database.database)});
477
+ define('DB_USER', ${phpString(database.user)});
478
+ define('DB_PASSWORD', ${phpString(database.password)});
479
+ define('DB_HOST', ${phpString(dbHost)});
480
+ define('DB_CHARSET', 'utf8');
481
+ define('DB_COLLATE', '');
482
+ define('WP_DEBUG', false);
483
+ define('WP_DEBUG_DISPLAY', false);
484
+ ${salts}
485
+
486
+ $table_prefix = 'wp_';
487
+
488
+ if ( ! defined('ABSPATH') ) {
489
+ define('ABSPATH', dirname(__FILE__) . '/');
490
+ }
491
+
492
+ require_once ABSPATH . 'wp-settings.php';
493
+ `;
494
+ }
495
+ async function prepareMysqlDatabase(database, options) {
496
+ if (process.env.PRESSSHIP_TEST_SKIP_MYSQL_PREP === "1") {
497
+ return;
498
+ }
499
+ try {
500
+ await createMysqlDatabase(database, options);
501
+ }
502
+ catch (error) {
503
+ if (shouldUseManagedMysqlFallback(database, error)) {
504
+ await configureManagedMysql(database);
505
+ await writeFile(database.configPath, createMysqlWpConfig(database), { mode: 0o600 });
506
+ await createMysqlDatabase(database, options);
507
+ return;
508
+ }
509
+ throw createMysqlPrepareError(database, error);
510
+ }
511
+ }
512
+ async function createMysqlDatabase(database, options) {
513
+ const databaseName = escapeMysqlIdentifier(database.database);
514
+ const sql = options.reset
515
+ ? `DROP DATABASE IF EXISTS ${databaseName};\nCREATE DATABASE ${databaseName} CHARACTER SET utf8 COLLATE utf8_general_ci;\n`
516
+ : `CREATE DATABASE IF NOT EXISTS ${databaseName} CHARACTER SET utf8 COLLATE utf8_general_ci;\n`;
517
+ const mysql = await import("mysql2/promise");
518
+ const connection = await mysql.createConnection({
519
+ host: database.host,
520
+ port: database.port,
521
+ user: database.user,
522
+ password: database.password,
523
+ multipleStatements: true
524
+ });
525
+ try {
526
+ await connection.query(sql);
527
+ }
528
+ finally {
529
+ await connection.end().catch(() => undefined);
530
+ }
531
+ }
532
+ async function configureManagedMysql(database) {
533
+ try {
534
+ const port = await ensureManagedMysqlContainer();
535
+ database.host = "127.0.0.1";
536
+ database.port = port;
537
+ database.user = "root";
538
+ database.password = managedMysqlPassword;
539
+ database.server = "managed-docker";
540
+ }
541
+ catch (error) {
542
+ throw new Error(`No MySQL server is reachable at ${database.host}:${database.port}, and Pressship could not start its managed MariaDB container. ` +
543
+ `Start Docker or OrbStack, or configure an external MySQL server in Settings. ${errorMessage(error)}`);
544
+ }
545
+ }
546
+ async function ensureManagedMysqlContainer() {
547
+ const existing = await inspectManagedMysqlContainer();
548
+ if (existing) {
549
+ if (!existing.running) {
550
+ await execa("docker", ["start", managedMysqlContainerName]);
551
+ }
552
+ await waitForManagedMysql(existing.port);
553
+ return existing.port;
554
+ }
555
+ const port = await getFreeLocalPort();
556
+ await execa("docker", [
557
+ "run",
558
+ "-d",
559
+ "--name",
560
+ managedMysqlContainerName,
561
+ "-p",
562
+ `127.0.0.1:${port}:3306`,
563
+ "-e",
564
+ `MARIADB_ROOT_PASSWORD=${managedMysqlPassword}`,
565
+ "-e",
566
+ "MARIADB_AUTO_UPGRADE=1",
567
+ "-e",
568
+ "MARIADB_INITDB_SKIP_TZINFO=1",
569
+ managedMysqlImage
570
+ ]);
571
+ await waitForManagedMysql(port);
572
+ return port;
573
+ }
574
+ async function inspectManagedMysqlContainer() {
575
+ const result = await execa("docker", ["container", "inspect", managedMysqlContainerName], { reject: false });
576
+ if (result.exitCode !== 0) {
577
+ return undefined;
578
+ }
579
+ const inspected = JSON.parse(result.stdout);
580
+ const container = inspected[0];
581
+ const hostPort = container?.NetworkSettings?.Ports?.["3306/tcp"]?.[0]?.HostPort;
582
+ const port = Number(hostPort);
583
+ if (!Number.isInteger(port) || port <= 0) {
584
+ await execa("docker", ["rm", "-f", managedMysqlContainerName], { reject: false });
585
+ return undefined;
586
+ }
587
+ return {
588
+ running: Boolean(container?.State?.Running),
589
+ port
590
+ };
591
+ }
592
+ async function waitForManagedMysql(port) {
593
+ const started = Date.now();
594
+ let lastError;
595
+ while (Date.now() - started < 90_000) {
596
+ try {
597
+ const mysql = await import("mysql2/promise");
598
+ const connection = await mysql.createConnection({
599
+ host: "127.0.0.1",
600
+ port,
601
+ user: "root",
602
+ password: managedMysqlPassword
603
+ });
604
+ await connection.end().catch(() => undefined);
605
+ return;
606
+ }
607
+ catch (error) {
608
+ lastError = error;
609
+ await new Promise((resolve) => setTimeout(resolve, 1000));
610
+ }
611
+ }
612
+ throw new Error(`Managed MariaDB did not become ready on 127.0.0.1:${port}. ${errorMessage(lastError)}`);
613
+ }
614
+ async function getFreeLocalPort() {
615
+ return new Promise((resolve, reject) => {
616
+ const server = createNetServer();
617
+ server.once("error", reject);
618
+ server.listen(0, "127.0.0.1", () => {
619
+ const address = server.address();
620
+ const port = typeof address === "object" && address ? address.port : undefined;
621
+ server.close((error) => {
622
+ if (error) {
623
+ reject(error);
624
+ return;
625
+ }
626
+ if (!port) {
627
+ reject(new Error("Could not reserve a local port for managed MariaDB."));
628
+ return;
629
+ }
630
+ resolve(port);
631
+ });
632
+ });
633
+ });
634
+ }
635
+ function shouldUseManagedMysqlFallback(database, error) {
636
+ return database.server === "external" && isLocalMysqlHost(database.host) && isConnectionUnavailable(error);
637
+ }
638
+ function isLocalMysqlHost(host) {
639
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
640
+ }
641
+ function isConnectionUnavailable(error) {
642
+ const message = errorMessage(error);
643
+ return (message.includes("ECONNREFUSED") ||
644
+ message.includes("ETIMEDOUT") ||
645
+ message.includes("ENOTFOUND") ||
646
+ message.includes("EHOSTUNREACH"));
647
+ }
648
+ function createMysqlPrepareError(database, error) {
649
+ return new Error(`Could not prepare MySQL database ${database.database} at ${database.host}:${database.port}. ` +
650
+ `Check the Playground MySQL settings and make sure the database server is running. ${errorMessage(error)}`);
651
+ }
652
+ function errorMessage(error) {
653
+ return error instanceof Error ? error.message : String(error);
654
+ }
655
+ function describePlaygroundDatabase(database) {
656
+ if (database.mode === "sqlite") {
657
+ return "SQLite";
658
+ }
659
+ if (database.server === "managed-docker") {
660
+ return `Managed MariaDB ${database.host}:${database.port}/${database.database}`;
661
+ }
662
+ return `MySQL ${database.user}@${database.host}:${database.port}/${database.database}`;
663
+ }
664
+ function escapeMysqlIdentifier(value) {
665
+ return `\`${value.replace(/`/g, "``")}\``;
666
+ }
667
+ function phpString(value) {
668
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
669
+ }
670
+ function randomSalt() {
671
+ return createHash("sha256").update(`${Date.now()}:${Math.random()}:${process.hrtime.bigint()}`).digest("hex");
672
+ }
197
673
  function isLocalPluginTarget(target) {
198
674
  return target === "." || target.startsWith("..") || target.startsWith(`.${path.sep}`) || path.isAbsolute(target) || pathExists(target);
199
675
  }
@@ -204,4 +680,17 @@ function normalizePlaygroundVersion(version) {
204
680
  const trimmed = version.trim();
205
681
  return trimmed && trimmed !== "0" ? trimmed : undefined;
206
682
  }
683
+ function parseWordPressBranch(version) {
684
+ const match = version?.trim().match(/^(\d+)(?:\.(\d+))?/);
685
+ if (!match) {
686
+ return undefined;
687
+ }
688
+ return {
689
+ major: Number(match[1]),
690
+ minor: Number(match[2] ?? 0)
691
+ };
692
+ }
693
+ function isWordPressBranchAtLeast(branch, major, minor) {
694
+ return branch.major > major || (branch.major === major && branch.minor >= minor);
695
+ }
207
696
  //# sourceMappingURL=demo.js.map