@wordpress/env 10.38.1-next.v.0 → 10.39.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.
@@ -0,0 +1,324 @@
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
+ * Checks a WordPress database connection. An error is thrown if the test is
52
+ * unsuccessful.
53
+ *
54
+ * @param {WPConfig} config The wp-env config object.
55
+ */
56
+ async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) {
57
+ await dockerCompose.run( 'cli', 'wp db check', {
58
+ config: dockerComposeConfigPath,
59
+ commandOptions: [ '--rm' ],
60
+ log: debug,
61
+ } );
62
+ }
63
+
64
+ /**
65
+ * Configures WordPress for the given environment by installing WordPress,
66
+ * activating all plugins, and activating the first theme. These steps are
67
+ * performed sequentially so as to not overload the WordPress instance.
68
+ *
69
+ * @param {WPEnvironment} environment The environment to configure. Either 'development' or 'tests'.
70
+ * @param {WPConfig} config The wp-env config object.
71
+ * @param {Object} spinner A CLI spinner which indicates progress.
72
+ */
73
+ async function configureWordPress( environment, config, spinner ) {
74
+ let wpVersion = '';
75
+ try {
76
+ wpVersion = await readWordPressVersion(
77
+ config.env[ environment ].coreSource,
78
+ spinner,
79
+ config.debug
80
+ );
81
+ } catch ( err ) {
82
+ // Ignore error.
83
+ }
84
+
85
+ // Create a project-specific wp-cli configuration, important for the `rewrite` command.
86
+ // Don't overwrite existing configuration.
87
+ const cliConfigCommand = `[ -f /var/www/html/wp-cli.yml ] || (
88
+ exec > /var/www/html/wp-cli.yml
89
+ echo "apache_modules:"
90
+ echo " - mod_rewrite"
91
+ )`;
92
+
93
+ const isMultisite = config.env[ environment ].multisite;
94
+
95
+ const installMethod = isMultisite ? 'multisite-install' : 'install';
96
+ 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`;
97
+
98
+ // -eo pipefail exits the command as soon as anything fails in bash.
99
+ const setupCommands = [
100
+ 'set -eo pipefail',
101
+ cliConfigCommand,
102
+ installCommand,
103
+ ];
104
+
105
+ // Bootstrap .htaccess for multisite
106
+ if ( isMultisite ) {
107
+ // Using a subshell with `exec` was the best tradeoff I could come up
108
+ // with between readability of this source and compatibility with the
109
+ // way that all strings in `setupCommands` are later joined with '&&'.
110
+ setupCommands.push(
111
+ `(
112
+ exec > /var/www/html/.htaccess
113
+ echo 'RewriteEngine On'
114
+ echo 'RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]'
115
+ echo 'RewriteBase /'
116
+ echo 'RewriteRule ^index\\.php$ - [L]'
117
+ echo ''
118
+ echo '# add a trailing slash to /wp-admin'
119
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]'
120
+ echo ''
121
+ echo 'RewriteCond %{REQUEST_FILENAME} -f [OR]'
122
+ echo 'RewriteCond %{REQUEST_FILENAME} -d'
123
+ echo 'RewriteRule ^ - [L]'
124
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]'
125
+ echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\\.php)$ $2 [L]'
126
+ echo 'RewriteRule . index.php [L]'
127
+ )`
128
+ );
129
+ }
130
+
131
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
132
+ const configAnchor =
133
+ wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' )
134
+ ? `"define('WP_DEBUG',"`
135
+ : `"define( 'WP_DEBUG',"`;
136
+
137
+ // Set wp-config.php values.
138
+ for ( let [ key, value ] of Object.entries(
139
+ config.env[ environment ].config
140
+ ) ) {
141
+ // Allow the configuration to skip a default constant by specifying it as null.
142
+ if ( null === value ) {
143
+ continue;
144
+ }
145
+
146
+ // Add quotes around string values to work with multi-word strings better.
147
+ value = typeof value === 'string' ? `"${ value }"` : value;
148
+ setupCommands.push(
149
+ `wp config set ${ key } ${ value } --anchor=${ configAnchor }${
150
+ typeof value !== 'string' ? ' --raw' : ''
151
+ }`
152
+ );
153
+ }
154
+
155
+ // Activate all plugins.
156
+ for ( const pluginSource of config.env[ environment ].pluginSources ) {
157
+ setupCommands.push( `wp plugin activate ${ pluginSource.basename }` );
158
+ }
159
+
160
+ if ( config.debug ) {
161
+ spinner.info(
162
+ `Running the following setup commands on the ${ environment } instance:\n - ${ setupCommands.join(
163
+ '\n - '
164
+ ) }\n`
165
+ );
166
+ }
167
+
168
+ // Execute all setup commands in a batch.
169
+ await dockerCompose.run(
170
+ environment === 'development' ? 'cli' : 'tests-cli',
171
+ [ 'bash', '-c', setupCommands.join( ' && ' ) ],
172
+ {
173
+ config: config.dockerComposeConfigPath,
174
+ commandOptions: [ '--rm' ],
175
+ log: config.debug,
176
+ }
177
+ );
178
+
179
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
180
+ // Additionally, WordPress versions below 5.4 used `dirname( __FILE__ )` instead of `__DIR__`.
181
+ let abspathDef = `define( 'ABSPATH', __DIR__ . '\\/' );`;
182
+ if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) ) {
183
+ abspathDef = `define('ABSPATH', dirname(__FILE__) . '\\/');`;
184
+ } else if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.4' ) ) {
185
+ abspathDef = `define( 'ABSPATH', dirname( __FILE__ ) . '\\/' );`;
186
+ }
187
+
188
+ // WordPress' PHPUnit suite expects a `wp-tests-config.php` in
189
+ // the directory that the test suite is contained within.
190
+ // Make sure ABSPATH points to the WordPress install.
191
+ await dockerCompose.exec(
192
+ environment === 'development' ? 'wordpress' : 'tests-wordpress',
193
+ [
194
+ 'sh',
195
+ '-c',
196
+ `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`,
197
+ ],
198
+ {
199
+ config: config.dockerComposeConfigPath,
200
+ log: config.debug,
201
+ }
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Resets the development server's database, the tests server's database, or both.
207
+ *
208
+ * @param {WPEnvironmentSelection} environment The environment to clean. Either 'development', 'tests', or 'all'.
209
+ * @param {WPConfig} config The wp-env config object.
210
+ */
211
+ async function resetDatabase(
212
+ environment,
213
+ { dockerComposeConfigPath, debug }
214
+ ) {
215
+ const options = {
216
+ config: dockerComposeConfigPath,
217
+ commandOptions: [ '--rm' ],
218
+ log: debug,
219
+ };
220
+
221
+ const tasks = [];
222
+
223
+ if ( environment === 'all' || environment === 'development' ) {
224
+ tasks.push( dockerCompose.run( 'cli', 'wp db reset --yes', options ) );
225
+ }
226
+
227
+ if ( environment === 'all' || environment === 'tests' ) {
228
+ tasks.push(
229
+ dockerCompose.run( 'tests-cli', 'wp db reset --yes', options )
230
+ );
231
+ }
232
+
233
+ await Promise.all( tasks );
234
+ }
235
+
236
+ /**
237
+ * Sets up WordPress directories, copying core files if needed.
238
+ *
239
+ * @param {WPConfig} config The wp-env config object.
240
+ */
241
+ async function setupWordPressDirectories( config ) {
242
+ if (
243
+ config.env.development.coreSource &&
244
+ hasSameCoreSource( [ config.env.development, config.env.tests ] )
245
+ ) {
246
+ await copyCoreFiles(
247
+ config.env.development.coreSource.path,
248
+ config.env.development.coreSource.testsPath
249
+ );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Returns true if all given environment configs have the same core source.
255
+ *
256
+ * @param {WPEnvironmentConfig[]} envs An array of environments to check.
257
+ *
258
+ * @return {boolean} True if all the environments have the same core source.
259
+ */
260
+ function hasSameCoreSource( envs ) {
261
+ if ( envs.length < 2 ) {
262
+ return true;
263
+ }
264
+ return ! envs.some( ( env ) =>
265
+ areCoreSourcesDifferent( envs[ 0 ].coreSource, env.coreSource )
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Checks if two core sources are different.
271
+ *
272
+ * @param {WPSource} coreSource1 First core source.
273
+ * @param {WPSource} coreSource2 Second core source.
274
+ * @return {boolean} True if the sources are different.
275
+ */
276
+ function areCoreSourcesDifferent( coreSource1, coreSource2 ) {
277
+ if (
278
+ ( ! coreSource1 && coreSource2 ) ||
279
+ ( coreSource1 && ! coreSource2 )
280
+ ) {
281
+ return true;
282
+ }
283
+
284
+ if ( coreSource1 && coreSource2 && coreSource1.path !== coreSource2.path ) {
285
+ return true;
286
+ }
287
+
288
+ return false;
289
+ }
290
+
291
+ /**
292
+ * Copies a WordPress installation, taking care to ignore large directories
293
+ * (.git, node_modules) and configuration files (wp-config.php).
294
+ *
295
+ * @param {string} fromPath Path to the WordPress directory to copy.
296
+ * @param {string} toPath Destination path.
297
+ */
298
+ async function copyCoreFiles( fromPath, toPath ) {
299
+ await copyDir( fromPath, toPath, {
300
+ filter( stat, filepath, filename ) {
301
+ if ( stat === 'symbolicLink' ) {
302
+ return false;
303
+ }
304
+ if ( stat === 'directory' && filename === '.git' ) {
305
+ return false;
306
+ }
307
+ if ( stat === 'directory' && filename === 'node_modules' ) {
308
+ return false;
309
+ }
310
+ if ( stat === 'file' && filename === 'wp-config.php' ) {
311
+ return false;
312
+ }
313
+ return true;
314
+ },
315
+ } );
316
+ }
317
+
318
+ module.exports = {
319
+ checkDatabaseConnection,
320
+ configureWordPress,
321
+ resetDatabase,
322
+ setupWordPressDirectories,
323
+ hasSameCoreSource,
324
+ };
@@ -0,0 +1,17 @@
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
+ module.exports = {
16
+ UnsupportedCommandError,
17
+ };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * External dependencies
5
+ */
6
+ const { existsSync } = require( 'fs' );
7
+ const path = require( 'path' );
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ const DockerRuntime = require( './docker' );
13
+ const PlaygroundRuntime = require( './playground' );
14
+ const { UnsupportedCommandError } = require( './errors' );
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
+ * Detect which runtime was used based on files in the work directory.
46
+ * Returns 'playground' if playground-blueprint.json exists, otherwise 'docker'.
47
+ *
48
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
49
+ * @return {string} Runtime name ('docker' or 'playground').
50
+ */
51
+ function detectRuntime( workDirectoryPath ) {
52
+ const playgroundBlueprintFile = path.join(
53
+ workDirectoryPath,
54
+ 'playground-blueprint.json'
55
+ );
56
+ if ( existsSync( playgroundBlueprintFile ) ) {
57
+ return 'playground';
58
+ }
59
+ return 'docker';
60
+ }
61
+
62
+ module.exports = {
63
+ getRuntime,
64
+ getAvailableRuntimes,
65
+ detectRuntime,
66
+ DockerRuntime,
67
+ PlaygroundRuntime,
68
+ UnsupportedCommandError,
69
+ };
@@ -0,0 +1,166 @@
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
+ for ( const theme of envConfig.themeSources || [] ) {
111
+ if ( theme.type === 'local' || theme.type === 'git' ) {
112
+ args.push(
113
+ '--mount-dir',
114
+ theme.path,
115
+ `/wordpress/wp-content/themes/${ theme.basename }`
116
+ );
117
+ } else {
118
+ throw new Error(
119
+ `Theme source "${ theme.basename || theme.path }" of type "${
120
+ theme.type
121
+ }" ` +
122
+ `is not supported with Playground runtime. Only local and git themes are supported.`
123
+ );
124
+ }
125
+ }
126
+
127
+ // Mount custom mappings
128
+ for ( const [ wpDir, source ] of Object.entries(
129
+ envConfig.mappings || {}
130
+ ) ) {
131
+ if ( source.type === 'local' || source.type === 'git' ) {
132
+ args.push( '--mount-dir', source.path, `/wordpress/${ wpDir }` );
133
+ } else {
134
+ throw new Error(
135
+ `Mapping source "${ source.path }" for "${ wpDir }" of type "${ source.type }" ` +
136
+ `is not supported with Playground runtime. Only local and git mappings are supported.`
137
+ );
138
+ }
139
+ }
140
+
141
+ // Mount core source if specified
142
+ if ( envConfig.coreSource ) {
143
+ if (
144
+ envConfig.coreSource.type === 'local' ||
145
+ envConfig.coreSource.type === 'git'
146
+ ) {
147
+ args.push(
148
+ '--mount-dir-before-install',
149
+ envConfig.coreSource.path,
150
+ '/wordpress'
151
+ );
152
+ } else {
153
+ throw new Error(
154
+ `Core source of type "${ envConfig.coreSource.type }" is not supported ` +
155
+ `with Playground runtime. Only local and git core sources are supported.`
156
+ );
157
+ }
158
+ }
159
+
160
+ return args;
161
+ }
162
+
163
+ module.exports = {
164
+ buildBlueprint,
165
+ getMountArgs,
166
+ };