@wordpress/env 10.38.1-next.v.0 → 11.0.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 (37) hide show
  1. package/README.md +158 -75
  2. package/lib/cli.js +85 -24
  3. package/lib/commands/clean.js +16 -38
  4. package/lib/commands/cleanup.js +79 -0
  5. package/lib/commands/destroy.js +33 -41
  6. package/lib/commands/index.js +6 -2
  7. package/lib/commands/logs.js +20 -62
  8. package/lib/commands/reset.js +46 -0
  9. package/lib/commands/run.js +16 -117
  10. package/lib/commands/start.js +78 -243
  11. package/lib/commands/status.js +160 -0
  12. package/lib/commands/stop.js +17 -19
  13. package/lib/config/get-config-from-environment-vars.js +2 -5
  14. package/lib/config/load-config.js +35 -5
  15. package/lib/config/parse-config.js +53 -21
  16. package/lib/config/post-process-config.js +38 -16
  17. package/lib/config/test/__snapshots__/config-integration.js.snap +16 -0
  18. package/lib/config/test/parse-config.js +33 -0
  19. package/lib/config/test/post-process-config.js +52 -0
  20. package/lib/download-sources.js +9 -53
  21. package/lib/{build-docker-compose-config.js → runtime/docker/build-docker-compose-config.js} +167 -128
  22. package/lib/runtime/docker/download-sources.js +63 -0
  23. package/lib/{download-wp-phpunit.js → runtime/docker/download-wp-phpunit.js} +4 -1
  24. package/lib/runtime/docker/index.js +863 -0
  25. package/lib/{init-config.js → runtime/docker/init-config.js} +22 -14
  26. package/lib/runtime/docker/wordpress.js +309 -0
  27. package/lib/runtime/errors.js +28 -0
  28. package/lib/runtime/index.js +91 -0
  29. package/lib/runtime/playground/blueprint-builder.js +158 -0
  30. package/lib/runtime/playground/index.js +530 -0
  31. package/lib/test/build-docker-compose-config.js +150 -3
  32. package/lib/test/cli.js +49 -13
  33. package/lib/wordpress.js +1 -297
  34. package/package.json +6 -3
  35. package/lib/commands/install-path.js +0 -21
  36. /package/lib/{get-host-user.js → runtime/docker/get-host-user.js} +0 -0
  37. /package/lib/{validate-run-container.js → runtime/docker/validate-run-container.js} +0 -0
@@ -10,11 +10,11 @@ const yaml = require( 'js-yaml' );
10
10
  /**
11
11
  * Internal dependencies
12
12
  */
13
- const { loadConfig, ValidationError } = require( './config' );
13
+ const { loadConfig, ValidationError } = require( '../../config' );
14
14
  const buildDockerComposeConfig = require( './build-docker-compose-config' );
15
15
 
16
16
  /**
17
- * @typedef {import('./config').WPConfig} WPConfig
17
+ * @typedef {import('../../config').WPConfig} WPConfig
18
18
  */
19
19
 
20
20
  /**
@@ -22,16 +22,17 @@ const buildDockerComposeConfig = require( './build-docker-compose-config' );
22
22
  * ./.wp-env.json, creates ~/.wp-env, ~/.wp-env/docker-compose.yml, and
23
23
  * ~/.wp-env/Dockerfile.
24
24
  *
25
- * @param {Object} options
26
- * @param {Object} options.spinner A CLI spinner which indicates progress.
27
- * @param {boolean} options.debug True if debug mode is enabled.
28
- * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off".
29
- * @param {string} options.spx The SPX mode to set. Defaults to "off".
30
- * @param {boolean} options.writeChanges If true, writes the parsed config to the
31
- * required docker files like docker-compose
32
- * and Dockerfile. By default, this is false
33
- * and only the `start` command writes any
34
- * changes.
25
+ * @param {Object} options
26
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
27
+ * @param {boolean} options.debug True if debug mode is enabled.
28
+ * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off".
29
+ * @param {string} options.spx The SPX mode to set. Defaults to "off".
30
+ * @param {boolean} options.writeChanges If true, writes the parsed config to the
31
+ * required docker files like docker-compose
32
+ * and Dockerfile. By default, this is false
33
+ * and only the `start` command writes any
34
+ * changes.
35
+ * @param {string|null} options.customConfigPath Path to a custom .wp-env.json configuration file.
35
36
  * @return {WPConfig} The-env config object.
36
37
  */
37
38
  module.exports = async function initConfig( {
@@ -40,8 +41,9 @@ module.exports = async function initConfig( {
40
41
  xdebug = 'off',
41
42
  spx = 'off',
42
43
  writeChanges = false,
44
+ customConfigPath = null,
43
45
  } ) {
44
- const config = await loadConfig( path.resolve( '.' ) );
46
+ const config = await loadConfig( path.resolve( '.' ), customConfigPath );
45
47
  config.debug = debug;
46
48
 
47
49
  // Adding this to the config allows the start command to understand that the
@@ -87,10 +89,16 @@ module.exports = async function initConfig( {
87
89
  yaml.dump( dockerComposeConfig )
88
90
  );
89
91
 
90
- // Write four Dockerfiles for each service we provided.
92
+ // Write Dockerfiles for each service we provided.
91
93
  // (WordPress and CLI services, then a development and test environment for each.)
92
94
  for ( const imageType of [ 'WordPress', 'CLI' ] ) {
93
95
  for ( const envType of [ 'development', 'tests' ] ) {
96
+ if (
97
+ envType === 'tests' &&
98
+ config.testsEnvironment === false
99
+ ) {
100
+ continue;
101
+ }
94
102
  await writeFile(
95
103
  path.resolve(
96
104
  config.workDirectoryPath,
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+ /**
3
+ * External dependencies
4
+ */
5
+ const util = require( 'util' );
6
+ const { v2: dockerCompose } = require( 'docker-compose' );
7
+
8
+ /**
9
+ * Promisified dependencies
10
+ */
11
+ const copyDir = util.promisify( require( 'copy-dir' ) );
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ const { readWordPressVersion } = require( '../../wordpress' );
17
+
18
+ /**
19
+ * @typedef {import('../../config').WPConfig} WPConfig
20
+ * @typedef {import('../../config').WPEnvironmentConfig} WPEnvironmentConfig
21
+ * @typedef {import('../../config').WPSource} WPSource
22
+ * @typedef {'development'|'tests'} WPEnvironment
23
+ * @typedef {'development'|'tests'|'all'} WPEnvironmentSelection
24
+ */
25
+
26
+ /**
27
+ * Utility function to check if a WordPress version is lower than another version.
28
+ *
29
+ * This is a non-comprehensive check only intended for this usage, to avoid pulling in a full semver library.
30
+ * It only considers the major and minor portions of the version and ignores the rest. Additionally, it assumes that
31
+ * the minor version is always a single digit (i.e. 0-9).
32
+ *
33
+ * Do not use this function for general version comparison, as it will not work for all cases.
34
+ *
35
+ * @param {string} version The version to check.
36
+ * @param {string} compareVersion The compare version to check whether the version is lower than.
37
+ * @return {boolean} True if the version is lower than the compare version, false otherwise.
38
+ */
39
+ function isWPMajorMinorVersionLower( version, compareVersion ) {
40
+ const versionNumber = Number.parseFloat(
41
+ version.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ]
42
+ );
43
+ const compareVersionNumber = Number.parseFloat(
44
+ compareVersion.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ]
45
+ );
46
+
47
+ return versionNumber < compareVersionNumber;
48
+ }
49
+
50
+ /**
51
+ * Configures WordPress for the given environment by installing WordPress,
52
+ * activating all plugins, and activating the first theme. These steps are
53
+ * performed sequentially so as to not overload the WordPress instance.
54
+ *
55
+ * @param {WPEnvironment} environment The environment to configure. Either 'development' or 'tests'.
56
+ * @param {WPConfig} config The wp-env config object.
57
+ * @param {Object} spinner A CLI spinner which indicates progress.
58
+ */
59
+ async function configureWordPress( environment, config, spinner ) {
60
+ let wpVersion = '';
61
+ try {
62
+ wpVersion = await readWordPressVersion(
63
+ config.env[ environment ].coreSource,
64
+ spinner,
65
+ config.debug
66
+ );
67
+ } catch ( err ) {
68
+ // Ignore error.
69
+ }
70
+
71
+ // Create a project-specific wp-cli configuration, important for the `rewrite` command.
72
+ // Don't overwrite existing configuration.
73
+ const cliConfigCommand = `[ -f /var/www/html/wp-cli.yml ] || (
74
+ exec > /var/www/html/wp-cli.yml
75
+ echo "apache_modules:"
76
+ echo " - mod_rewrite"
77
+ )`;
78
+
79
+ const isMultisite = config.env[ environment ].multisite;
80
+
81
+ const installMethod = isMultisite ? 'multisite-install' : 'install';
82
+ const installCommand = `wp core ${ installMethod } --url="${ config.env[ environment ].config.WP_SITEURL }" --title="${ config.name }" --admin_user=admin --admin_password=password --admin_email=wordpress@example.com --skip-email`;
83
+
84
+ // -eo pipefail exits the command as soon as anything fails in bash.
85
+ const setupCommands = [
86
+ 'set -eo pipefail',
87
+ cliConfigCommand,
88
+ installCommand,
89
+ ];
90
+
91
+ // Bootstrap .htaccess for multisite
92
+ if ( isMultisite ) {
93
+ // Using a subshell with `exec` was the best tradeoff I could come up
94
+ // with between readability of this source and compatibility with the
95
+ // way that all strings in `setupCommands` are later joined with '&&'.
96
+ setupCommands.push(
97
+ `(
98
+ exec > /var/www/html/.htaccess
99
+ echo 'RewriteEngine On'
100
+ echo 'RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]'
101
+ echo 'RewriteBase /'
102
+ echo 'RewriteRule ^index\\.php$ - [L]'
103
+ echo ''
104
+ echo '# add a trailing slash to /wp-admin'
105
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]'
106
+ echo ''
107
+ echo 'RewriteCond %{REQUEST_FILENAME} -f [OR]'
108
+ echo 'RewriteCond %{REQUEST_FILENAME} -d'
109
+ echo 'RewriteRule ^ - [L]'
110
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]'
111
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\\.php)$ $2 [L]'
112
+ echo 'RewriteRule . index.php [L]'
113
+ )`
114
+ );
115
+ }
116
+
117
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
118
+ const configAnchor =
119
+ wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' )
120
+ ? `"define('WP_DEBUG',"`
121
+ : `"define( 'WP_DEBUG',"`;
122
+
123
+ // Set wp-config.php values.
124
+ for ( let [ key, value ] of Object.entries(
125
+ config.env[ environment ].config
126
+ ) ) {
127
+ // Allow the configuration to skip a default constant by specifying it as null.
128
+ if ( null === value ) {
129
+ continue;
130
+ }
131
+
132
+ // Add quotes around string values to work with multi-word strings better.
133
+ value = typeof value === 'string' ? `"${ value }"` : value;
134
+ setupCommands.push(
135
+ `wp config set ${ key } ${ value } --anchor=${ configAnchor }${
136
+ typeof value !== 'string' ? ' --raw' : ''
137
+ }`
138
+ );
139
+ }
140
+
141
+ // Activate all plugins.
142
+ for ( const pluginSource of config.env[ environment ].pluginSources ) {
143
+ setupCommands.push( `wp plugin activate ${ pluginSource.basename }` );
144
+ }
145
+
146
+ if ( config.debug ) {
147
+ spinner.info(
148
+ `Running the following setup commands on the ${ environment } instance:\n - ${ setupCommands.join(
149
+ '\n - '
150
+ ) }\n`
151
+ );
152
+ }
153
+
154
+ // Execute all setup commands in a batch.
155
+ await dockerCompose.run(
156
+ environment === 'development' ? 'cli' : 'tests-cli',
157
+ [ 'bash', '-c', setupCommands.join( ' && ' ) ],
158
+ {
159
+ config: config.dockerComposeConfigPath,
160
+ commandOptions: [ '--rm' ],
161
+ log: config.debug,
162
+ }
163
+ );
164
+
165
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
166
+ // Additionally, WordPress versions below 5.4 used `dirname( __FILE__ )` instead of `__DIR__`.
167
+ let abspathDef = `define( 'ABSPATH', __DIR__ . '\\/' );`;
168
+ if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) ) {
169
+ abspathDef = `define('ABSPATH', dirname(__FILE__) . '\\/');`;
170
+ } else if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.4' ) ) {
171
+ abspathDef = `define( 'ABSPATH', dirname( __FILE__ ) . '\\/' );`;
172
+ }
173
+
174
+ // WordPress' PHPUnit suite expects a `wp-tests-config.php` in
175
+ // the directory that the test suite is contained within.
176
+ // Make sure ABSPATH points to the WordPress install.
177
+ await dockerCompose.exec(
178
+ environment === 'development' ? 'wordpress' : 'tests-wordpress',
179
+ [
180
+ 'sh',
181
+ '-c',
182
+ `sed -e "/^require.*wp-settings.php/d" -e "s/${ abspathDef }/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`,
183
+ ],
184
+ {
185
+ config: config.dockerComposeConfigPath,
186
+ log: config.debug,
187
+ }
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Resets the development server's database, the tests server's database, or both.
193
+ *
194
+ * @param {WPEnvironmentSelection} environment The environment to clean. Either 'development', 'tests', or 'all'.
195
+ * @param {WPConfig} config The wp-env config object.
196
+ */
197
+ async function resetDatabase(
198
+ environment,
199
+ { dockerComposeConfigPath, debug }
200
+ ) {
201
+ const options = {
202
+ config: dockerComposeConfigPath,
203
+ commandOptions: [ '--rm' ],
204
+ log: debug,
205
+ };
206
+
207
+ const tasks = [];
208
+
209
+ if ( environment === 'all' || environment === 'development' ) {
210
+ tasks.push( dockerCompose.run( 'cli', 'wp db reset --yes', options ) );
211
+ }
212
+
213
+ if ( environment === 'all' || environment === 'tests' ) {
214
+ tasks.push(
215
+ dockerCompose.run( 'tests-cli', 'wp db reset --yes', options )
216
+ );
217
+ }
218
+
219
+ await Promise.all( tasks );
220
+ }
221
+
222
+ /**
223
+ * Sets up WordPress directories, copying core files if needed.
224
+ *
225
+ * @param {WPConfig} config The wp-env config object.
226
+ */
227
+ async function setupWordPressDirectories( config ) {
228
+ if (
229
+ config.env.development.coreSource &&
230
+ hasSameCoreSource( [ config.env.development, config.env.tests ] )
231
+ ) {
232
+ await copyCoreFiles(
233
+ config.env.development.coreSource.path,
234
+ config.env.development.coreSource.testsPath
235
+ );
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Returns true if all given environment configs have the same core source.
241
+ *
242
+ * @param {WPEnvironmentConfig[]} envs An array of environments to check.
243
+ *
244
+ * @return {boolean} True if all the environments have the same core source.
245
+ */
246
+ function hasSameCoreSource( envs ) {
247
+ if ( envs.length < 2 ) {
248
+ return true;
249
+ }
250
+ return ! envs.some( ( env ) =>
251
+ areCoreSourcesDifferent( envs[ 0 ].coreSource, env.coreSource )
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Checks if two core sources are different.
257
+ *
258
+ * @param {WPSource} coreSource1 First core source.
259
+ * @param {WPSource} coreSource2 Second core source.
260
+ * @return {boolean} True if the sources are different.
261
+ */
262
+ function areCoreSourcesDifferent( coreSource1, coreSource2 ) {
263
+ if (
264
+ ( ! coreSource1 && coreSource2 ) ||
265
+ ( coreSource1 && ! coreSource2 )
266
+ ) {
267
+ return true;
268
+ }
269
+
270
+ if ( coreSource1 && coreSource2 && coreSource1.path !== coreSource2.path ) {
271
+ return true;
272
+ }
273
+
274
+ return false;
275
+ }
276
+
277
+ /**
278
+ * Copies a WordPress installation, taking care to ignore large directories
279
+ * (.git, node_modules) and configuration files (wp-config.php).
280
+ *
281
+ * @param {string} fromPath Path to the WordPress directory to copy.
282
+ * @param {string} toPath Destination path.
283
+ */
284
+ async function copyCoreFiles( fromPath, toPath ) {
285
+ await copyDir( fromPath, toPath, {
286
+ filter( stat, filepath, filename ) {
287
+ if ( stat === 'symbolicLink' ) {
288
+ return false;
289
+ }
290
+ if ( stat === 'directory' && filename === '.git' ) {
291
+ return false;
292
+ }
293
+ if ( stat === 'directory' && filename === 'node_modules' ) {
294
+ return false;
295
+ }
296
+ if ( stat === 'file' && filename === 'wp-config.php' ) {
297
+ return false;
298
+ }
299
+ return true;
300
+ },
301
+ } );
302
+ }
303
+
304
+ module.exports = {
305
+ configureWordPress,
306
+ resetDatabase,
307
+ setupWordPressDirectories,
308
+ hasSameCoreSource,
309
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Error thrown when a command is not supported by the current runtime.
5
+ */
6
+ class UnsupportedCommandError extends Error {
7
+ constructor( command ) {
8
+ super(
9
+ `The '${ command }' command is not supported in the Playground runtime at the moment.`
10
+ );
11
+ this.name = 'UnsupportedCommandError';
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Error thrown when the environment has not been initialized.
17
+ */
18
+ class EnvironmentNotInitializedError extends Error {
19
+ constructor() {
20
+ super( 'Environment not initialized. Run `wp-env start` first.' );
21
+ this.name = 'EnvironmentNotInitializedError';
22
+ }
23
+ }
24
+
25
+ module.exports = {
26
+ UnsupportedCommandError,
27
+ EnvironmentNotInitializedError,
28
+ };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Internal dependencies
5
+ */
6
+ const DockerRuntime = require( './docker' );
7
+ const PlaygroundRuntime = require( './playground' );
8
+ const {
9
+ UnsupportedCommandError,
10
+ EnvironmentNotInitializedError,
11
+ } = require( './errors' );
12
+ const { setCache, getCache } = require( '../cache' );
13
+
14
+ const RUNTIME_CACHE_KEY = 'runtime';
15
+
16
+ const runtimes = {
17
+ docker: DockerRuntime,
18
+ playground: PlaygroundRuntime,
19
+ };
20
+
21
+ /**
22
+ * Get a runtime instance by name.
23
+ *
24
+ * @param {string} name Runtime name ('docker' or 'playground').
25
+ * @return {Object} Runtime instance.
26
+ */
27
+ function getRuntime( name ) {
28
+ const RuntimeClass = runtimes[ name ];
29
+ if ( ! RuntimeClass ) {
30
+ throw new Error( `Unknown runtime: ${ name }` );
31
+ }
32
+ return new RuntimeClass();
33
+ }
34
+
35
+ /**
36
+ * Get all available runtime names.
37
+ *
38
+ * @return {string[]} Array of runtime names.
39
+ */
40
+ function getAvailableRuntimes() {
41
+ return Object.keys( runtimes );
42
+ }
43
+
44
+ /**
45
+ * Save the runtime type to the cache file.
46
+ * Called when start command initializes the environment.
47
+ *
48
+ * @param {string} runtimeName The runtime name ('docker' or 'playground').
49
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
50
+ */
51
+ async function saveRuntime( runtimeName, workDirectoryPath ) {
52
+ await setCache( RUNTIME_CACHE_KEY, runtimeName, { workDirectoryPath } );
53
+ }
54
+
55
+ /**
56
+ * Get the saved runtime type from cache.
57
+ *
58
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
59
+ * @return {Promise<string|undefined>} The saved runtime name, or undefined if not set.
60
+ */
61
+ async function getSavedRuntime( workDirectoryPath ) {
62
+ return await getCache( RUNTIME_CACHE_KEY, { workDirectoryPath } );
63
+ }
64
+
65
+ /**
66
+ * Detect which runtime was used by reading from the cache.
67
+ * Throws EnvironmentNotInitializedError if no runtime has been saved.
68
+ *
69
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
70
+ * @return {Promise<string>} Runtime name ('docker' or 'playground').
71
+ * @throws {EnvironmentNotInitializedError} If environment not initialized.
72
+ */
73
+ async function detectRuntime( workDirectoryPath ) {
74
+ const savedRuntime = await getSavedRuntime( workDirectoryPath );
75
+ if ( ! savedRuntime ) {
76
+ throw new EnvironmentNotInitializedError();
77
+ }
78
+ return savedRuntime;
79
+ }
80
+
81
+ module.exports = {
82
+ getRuntime,
83
+ getAvailableRuntimes,
84
+ detectRuntime,
85
+ saveRuntime,
86
+ getSavedRuntime,
87
+ DockerRuntime,
88
+ PlaygroundRuntime,
89
+ UnsupportedCommandError,
90
+ EnvironmentNotInitializedError,
91
+ };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Builds a Playground Blueprint from wp-env configuration.
5
+ *
6
+ * @param {Object} config The wp-env config object.
7
+ * @return {Object} Playground Blueprint JSON object.
8
+ */
9
+ function buildBlueprint( config ) {
10
+ const envConfig = config.env.development;
11
+
12
+ const blueprint = {
13
+ $schema: 'https://playground.wordpress.net/blueprint-schema.json',
14
+ landingPage: '/wp-admin/',
15
+ preferredVersions: {
16
+ php: envConfig.phpVersion || '8.2',
17
+ wp: 'latest',
18
+ },
19
+ steps: [],
20
+ };
21
+
22
+ // Login step - matches wp-env default credentials
23
+ blueprint.steps.push( {
24
+ step: 'login',
25
+ username: 'admin',
26
+ password: 'password',
27
+ } );
28
+
29
+ // Add plugins
30
+ for ( const plugin of envConfig.pluginSources || [] ) {
31
+ if ( plugin.type === 'local' || plugin.type === 'git' ) {
32
+ // Local and git plugins are mounted via CLI args, just activate
33
+ blueprint.steps.push( {
34
+ step: 'activatePlugin',
35
+ pluginPath: `/wordpress/wp-content/plugins/${ plugin.basename }`,
36
+ } );
37
+ } else if ( plugin.type === 'zip' && plugin.url ) {
38
+ blueprint.steps.push( {
39
+ step: 'installPlugin',
40
+ pluginData: { resource: 'url', url: plugin.url },
41
+ options: { activate: true },
42
+ } );
43
+ } else {
44
+ throw new Error(
45
+ `Plugin source "${ plugin.basename || plugin.path }" of type "${
46
+ plugin.type
47
+ }" ` +
48
+ `is not supported with Playground runtime. Only local, git, and zip plugins are supported.`
49
+ );
50
+ }
51
+ }
52
+
53
+ // Note: Themes are mounted via CLI args but NOT activated.
54
+ // This matches Docker runtime behavior where WordPress uses its default theme.
55
+ // Users can activate themes manually or via wp-cli if needed.
56
+
57
+ // Configure wp-config constants
58
+ const wpConfigConsts = {};
59
+ for ( const [ key, value ] of Object.entries( envConfig.config || {} ) ) {
60
+ if ( value !== null ) {
61
+ wpConfigConsts[ key ] = value;
62
+ }
63
+ }
64
+ if ( Object.keys( wpConfigConsts ).length > 0 ) {
65
+ blueprint.steps.push( {
66
+ step: 'defineWpConfigConsts',
67
+ consts: wpConfigConsts,
68
+ } );
69
+ }
70
+
71
+ // Handle multisite
72
+ if ( envConfig.multisite ) {
73
+ blueprint.steps.push( {
74
+ step: 'enableMultisite',
75
+ } );
76
+ }
77
+
78
+ return blueprint;
79
+ }
80
+
81
+ /**
82
+ * Get mount arguments for the Playground CLI.
83
+ *
84
+ * @param {Object} config The wp-env config object.
85
+ * @return {string[]} Array of mount arguments.
86
+ */
87
+ function getMountArgs( config ) {
88
+ const args = [];
89
+ const envConfig = config.env.development;
90
+
91
+ // Mount plugins
92
+ for ( const plugin of envConfig.pluginSources || [] ) {
93
+ if ( plugin.type === 'local' || plugin.type === 'git' ) {
94
+ args.push(
95
+ '--mount-dir',
96
+ plugin.path,
97
+ `/wordpress/wp-content/plugins/${ plugin.basename }`
98
+ );
99
+ } else if ( plugin.type !== 'zip' ) {
100
+ throw new Error(
101
+ `Plugin source "${ plugin.basename || plugin.path }" of type "${
102
+ plugin.type
103
+ }" ` +
104
+ `is not supported with Playground runtime. Only local, git, and zip plugins are supported.`
105
+ );
106
+ }
107
+ }
108
+
109
+ // Mount themes
110
+ // All theme types (local, git, zip) can be mounted after downloading/extraction
111
+ for ( const theme of envConfig.themeSources || [] ) {
112
+ args.push(
113
+ '--mount-dir',
114
+ theme.path,
115
+ `/wordpress/wp-content/themes/${ theme.basename }`
116
+ );
117
+ }
118
+
119
+ // Mount custom mappings
120
+ // All source types (local, git, zip) can be mounted after downloading/extraction
121
+ for ( const [ wpDir, source ] of Object.entries(
122
+ envConfig.mappings || {}
123
+ ) ) {
124
+ args.push( '--mount-dir', source.path, `/wordpress/${ wpDir }` );
125
+ }
126
+
127
+ // Translate core source to Playground's --wp flag or mount it.
128
+ if ( envConfig.coreSource ) {
129
+ if ( envConfig.coreSource.type === 'zip' && envConfig.coreSource.url ) {
130
+ // For zip URLs, let Playground download WordPress natively.
131
+ args.push( '--wp', envConfig.coreSource.url );
132
+ } else if ( envConfig.coreSource.type === 'git' ) {
133
+ // For git sources, pass the version ref to Playground's --wp flag.
134
+ // e.g., WordPress/WordPress#6.5 → --wp 6.5
135
+ // WordPress/WordPress (no ref) → default "latest", no flag needed.
136
+ if ( envConfig.coreSource.ref ) {
137
+ args.push( '--wp', envConfig.coreSource.ref );
138
+ }
139
+ } else {
140
+ // For local sources, mount the directory and tell Playground to
141
+ // use the existing files instead of downloading its own copy.
142
+ args.push(
143
+ '--mount-dir-before-install',
144
+ envConfig.coreSource.path,
145
+ '/wordpress',
146
+ '--wordpress-install-mode',
147
+ 'install-from-existing-files-if-needed'
148
+ );
149
+ }
150
+ }
151
+
152
+ return args;
153
+ }
154
+
155
+ module.exports = {
156
+ buildBlueprint,
157
+ getMountArgs,
158
+ };