@@ -426,6 +426,7 @@ interface RecipeDryRunExtraPlugin {
426426 target : string
427427 pluginFile : string
428428 activate : boolean
429+ loadAs : "plugin " | "mu-plugin"
429430 provenance : RecipeSourceProvenance
430431}
431432
@@ -534,6 +535,7 @@ interface PreparedExtraPlugin {
534535 target : string
535536 pluginFile : string
536537 activate : boolean
538+ loadAs : "plugin" | "mu-plugin"
537539 cleanupPaths : string [ ]
538540 provenance : RecipeSourceProvenance
539541}
@@ -1428,6 +1430,11 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
14281430 interruption ?. throwIfInterrupted ( )
14291431 }
14301432
1433+ const muPluginInstallCode = installMuPluginsCode(extraPlugins)
1434+ if (muPluginInstallCode) {
1435+ executions . push ( withRecipeExecutionPhase ( await runtime . execute ( { command : "wordpress.run-php" , args : [ `code=${ muPluginInstallCode } ` ] } ) , "setup" , - 2 ) )
1436+ }
1437+
14311438 const pluginActivationCode = activateExtraPluginsCode(extraPlugins)
14321439 if (pluginActivationCode) {
14331440 executions . push ( withRecipeExecutionPhase ( await awaitRecipe ( runtime . execute ( { command : "wordpress.run-php" , args : [ `code=${ pluginActivationCode } ` ] } ) ) , "setup" , - 1 ) )
@@ -3247,6 +3254,10 @@ function parseWorkspaceRecipe(raw: string, recipePath: string): WorkspaceRecipe
32473254 if (plugin.slug && ! / ^ [ a - z0 - 9 ] [ a - z0 - 9 - _ ] * $ / i . test ( plugin . slug ) ) {
32483255 throw new Error ( `Recipe extra_plugins slug must be a plugin-directory slug: ${ recipePath } ` )
32493256 }
3257+
3258+ if (plugin.loadAs && plugin . loadAs !== "plugin " && plugin . loadAs !== "mu - plugin ") {
3259+ throw new Error ( `Recipe extra_plugins loadAs must be plugin or mu-plugin: ${ recipePath } ` )
3260+ }
32503261 }
32513262
32523263 const siteSeeds = recipe . inputs ?. siteSeeds ?? [ ]
@@ -3639,13 +3650,18 @@ async function recipeDryRunSteps(recipe: WorkspaceRecipe, recipeDirectory: strin
36393650 return {
36403651 source : plugin . source ,
36413652 slug,
3642- target : `/wordpress/wp-content/plugins/ ${ slug } ` ,
3653+ target : pluginTarget ( slug , plugin . loadAs ?? "plugin" ) ,
36433654 pluginFile : await resolveRecipeExtraPluginFile ( plugin , recipeDirectory ) ,
36443655 activate : plugin . activate !== false ,
3656+ loadAs : plugin . loadAs ?? "plugin" ,
36453657 cleanupPaths : [ ] ,
36463658 provenance : recipeSourceProvenance ( recipeSource ( plugin . source , plugin . sha256 ) , recipeDirectory ) ,
36473659 }
36483660 } ) )
3661+ const muPluginInstallCode = installMuPluginsCode ( dryRunExtraPlugins )
3662+ if ( muPluginInstallCode ) {
3663+ steps . push ( recipeDryRunStep ( { command : "wordpress.run-php" , args : [ `code=${ muPluginInstallCode } ` ] } , recipeDirectory , policy , "setup" , - 2 , "install-mu-plugins" ) )
3664+ }
36493665 const pluginActivationCode = activateExtraPluginsCode ( dryRunExtraPlugins )
36503666 if ( pluginActivationCode ) {
36513667 steps . push ( recipeDryRunStep ( { command : "wordpress.run-php" , args : [ `code=${ pluginActivationCode } ` ] } , recipeDirectory , policy , "setup" , - 1 , "activate-extra-plugins" ) )
@@ -3725,8 +3741,10 @@ function recipeRunMetadata(recipe: WorkspaceRecipe, recipePath: string, workspac
37253741 const extraPluginMetadata = extraPlugins . map ( ( plugin ) => ( {
37263742 source : plugin . source ,
37273743 slug : plugin . slug ,
3744+ target : plugin . target ,
37283745 pluginFile : plugin . pluginFile ,
37293746 activate : plugin . activate ,
3747+ loadAs : plugin . loadAs ,
37303748 provenance : plugin . provenance ,
37313749 } ) )
37323750 const siteSeedProvenance = recipeDryRunSiteSeeds ( recipe , dirname ( recipePath ) )
@@ -3828,9 +3846,10 @@ function recipeDryRunExtraPlugins(recipe: WorkspaceRecipe, recipeDirectory: stri
38283846 sourceRef : plugin . source ,
38293847 sourceType : source . type ,
38303848 slug,
3831- target : `/wordpress/wp-content/plugins/ ${ slug } ` ,
3849+ target : pluginTarget ( slug , plugin . loadAs ?? "plugin" ) ,
38323850 pluginFile : recipeExtraPluginFile ( plugin ) ,
38333851 activate : plugin . activate !== false ,
3852+ loadAs : plugin . loadAs ?? "plugin" ,
38343853 provenance,
38353854 }
38363855 } )
@@ -4433,13 +4452,15 @@ async function prepareRecipeExtraPlugins(recipe: WorkspaceRecipe, recipeDirector
44334452 const slug = recipeExtraPluginSlug ( plugin )
44344453 const resolved = await prepareRecipeSource ( plugin . source , recipeDirectory , slug , plugin . sha256 )
44354454 const pluginFile = await resolveRecipeExtraPluginFile ( plugin , recipeDirectory )
4455+ const loadAs = plugin . loadAs ?? "plugin"
44364456 await assertPreparedPluginFileExists ( resolved . source , pluginFile . slice ( slug . length + 1 ) , plugin . source )
44374457 plugins . push ( {
44384458 source : resolved . source ,
44394459 slug,
4440- target : `/wordpress/wp-content/plugins/ ${ slug } ` ,
4460+ target : pluginTarget ( slug , loadAs ) ,
44414461 pluginFile,
44424462 activate : plugin . activate !== false ,
4463+ loadAs,
44434464 cleanupPaths : resolved . cleanupPaths ,
44444465 provenance : resolved . provenance ,
44454466 } )
@@ -4854,6 +4875,14 @@ function recipeExtraPluginFile(plugin: WorkspaceRecipeExtraPlugin): string {
48544875 return plugin . pluginFile ?? `${ slug } /${ slug } .php`
48554876}
48564877
4878+ function pluginTarget(slug: string, loadAs: PreparedExtraPlugin["loadAs"]): string {
4879+ if ( loadAs === "mu-plugin" ) {
4880+ return `/wordpress/wp-content/mu-plugins/wp-codebox-runtime/${ slug } `
4881+ }
4882+
4883+ return `/wordpress/wp-content/plugins/${ slug } `
4884+ }
4885+
48574886async function resolveRecipeExtraPluginFile ( plugin : WorkspaceRecipeExtraPlugin , recipeDirectory : string ) : Promise < string > {
48584887 const slug = recipeExtraPluginSlug ( plugin )
48594888 if ( plugin . pluginFile ) {
@@ -4880,7 +4909,7 @@ async function resolveRecipeExtraPluginFile(plugin: WorkspaceRecipeExtraPlugin,
48804909
48814910function activateExtraPluginsCode ( extraPlugins : PreparedExtraPlugin [ ] ) : string | null {
48824911 const pluginFiles = extraPlugins
4883- . filter ( ( plugin ) => plugin . activate !== false )
4912+ . filter ( ( plugin ) => plugin . loadAs === "plugin" && plugin . activate !== false )
48844913 . map ( ( plugin ) => plugin . pluginFile )
48854914
48864915 if ( pluginFiles . length === 0 ) {
@@ -4906,6 +4935,55 @@ foreach ($plugins as $plugin) {
49064935echo wp_json_encode ( array ( 'command ' => 'activate-extra-plugins' , 'plugins' = > $activated ) , JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ; `
49074936}
49084937
4938+ function installMuPluginsCode(extraPlugins: PreparedExtraPlugin[]): string | null {
4939+ const muPlugins = extraPlugins
4940+ .filter((plugin) => plugin.loadAs === "mu-plugin")
4941+ .map((plugin) => plugin.pluginFile)
4942+
4943+ if (muPlugins.length === 0) {
4944+ return null
4945+ }
4946+
4947+ return ` $plugins = ${JSON . stringify ( muPlugins ) } ;
4948+ $runtime_dir = WPMU_PLUGIN_DIR . '/wp-codebox-runtime' ;
4949+ if ( ! is_dir ( WPMU_PLUGIN_DIR ) && ! mkdir ( WPMU_PLUGIN_DIR , 0777 , true ) && ! is_dir ( WPMU_PLUGIN_DIR ) ) {
4950+ throw new RuntimeException ( 'Could not create mu-plugins directory.' ) ;
4951+ }
4952+ if ( ! is_dir ( $runtime_dir ) ) {
4953+ throw new RuntimeException ( 'WP Codebox runtime mu-plugin directory is not mounted.' ) ;
4954+ }
4955+ $loader = WPMU_PLUGIN_DIR . '/wp-codebox-runtime-loader.php' ;
4956+ $lines = array (
4957+ '<?php' ,
4958+ '/**' ,
4959+ ' * Plugin Name: WP Codebox Runtime Loader' ,
4960+ ' * Description: Loads WP Codebox runtime substrate as must-use plugins.' ,
4961+ ' */' ,
4962+ '' ,
4963+ "defined( 'ABSPATH' ) || exit;" ,
4964+ '' ,
4965+ "if ( ! defined( 'DATAMACHINE_WORKSPACE_PATH' ) ) {" ,
4966+ " define( 'DATAMACHINE_WORKSPACE_PATH', ${JSON.stringify(SANDBOX_WORKSPACE_ROOT)} );" ,
4967+ "}" ,
4968+ "add_filter( 'datamachine_should_load_full_runtime', '__return_true', 1 );" ,
4969+ '' ,
4970+ ) ;
4971+ foreach ( $plugins as $plugin ) {
4972+ if ( '' === $plugin || str_starts_with ( $plugin , '/' ) || str_contains ( $plugin , '..' ) || ! str_ends_with ( $plugin , '.php' ) ) {
4973+ throw new RuntimeException ( 'Unsafe WP Codebox runtime mu-plugin entry.' ) ;
4974+ }
4975+ $plugin_file = $runtime_dir . '/ ' . $plugin ;
4976+ if ( ! file_exists ( $plugin_file ) ) {
4977+ throw new RuntimeException ( sprintf ( 'WP Codebox runtime mu-plugin is not mounted: %s' , $plugin ) ) ;
4978+ }
4979+ $lines [ ] = "require_once WPMU_PLUGIN_DIR . '/wp-codebox-runtime/" . str_replace ( "'" , "\\'" , $plugin ) . "'; ";
4980+ }
4981+ if ( false === file_put_contents ( $loader , implode ( "\\n" , $lines ) . "\\n" ) ) {
4982+ throw new RuntimeException ( 'Could not write WP Codebox runtime mu-plugin loader.' ) ;
4983+ }
4984+ echo wp_json_encode ( array ( 'command' = > 'install-mu-plugins' , 'plugins' = > $plugins , 'loader' = > $loader ) , JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ; `
4985+ }
4986+
49094987function parseMount(value: string): RunOptions["mounts"][number] {
49104988 const [source, target, mode = "readwrite"] = value.split(":")
49114989
0 commit comments