55 */
66import fs from 'node:fs' ;
77import path from 'node:path' ;
8+ import { createHash } from 'node:crypto' ;
89import { fileURLToPath } from 'node:url' ;
910import { createRequire } from 'node:module' ;
1011
@@ -403,6 +404,15 @@ export async function runFixtureMatrixBatch({ fixtures, batchIndex, matrix, outp
403404 entrypoint : matrix . entrypoint ,
404405 fixtures,
405406 } ) ;
407+ // Discovery is deliberately a separate, short-lived Codebox runtime. It only
408+ // asks SSI for its registry-derived plan; package resolution happens on the
409+ // host while assembling the following fresh import runtime.
410+ const dependencyPlan = options . hostDependencyOrchestration
411+ ? await discoverFixtureDependencyPlan ( { fixtures, outputDirectory, staticSiteImporterPath, options, batchSuffix } )
412+ : undefined ;
413+ const resolvedDependencyPlan = dependencyPlan
414+ ? await resolveHostDependencyPlan ( dependencyPlan , path . join ( outputDirectory , 'dependency-cache' ) )
415+ : undefined ;
406416 const batchRecipe = buildFixtureMatrixRecipe ( {
407417 matrix : batchMatrix ,
408418 runId : batchMatrix . id ,
@@ -413,6 +423,7 @@ export async function runFixtureMatrixBatch({ fixtures, batchIndex, matrix, outp
413423 staticSiteImporterPath,
414424 staticSiteImporterPlugin : options . staticSiteImporterPlugin ,
415425 staticSiteImporterSlug : options . staticSiteImporterSlug ,
426+ dependencyPlan : resolvedDependencyPlan ,
416427 dependencyOverrides : prepareDependencyOverrides ( options ) ,
417428 svgFontEvidence : true ,
418429 ...fixtureMatrixRecipeInput ( normalizeFixtureMatrixRunConfig ( Object . fromEntries ( Object . keys ( FIXTURE_MATRIX_RUN_FIELDS ) . map ( ( key ) => [ key , options [ key ] ] ) ) ) ) ,
@@ -560,6 +571,104 @@ export async function runFixtureMatrixBatch({ fixtures, batchIndex, matrix, outp
560571 } ;
561572}
562573
574+ export async function resolveHostDependencyPlan ( plan , cacheDirectory , fetcher = fetch ) {
575+ const entries = [ ] ;
576+ for ( const entry of plan . entries ) {
577+ if ( entry . source_kind !== 'wordpress.org-plugin' || ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 - _ ] * $ / i. test ( entry . slug || '' ) ) throw new Error ( 'Host dependency resolver received an invalid plugin declaration.' ) ;
578+ const infoUrl = new URL ( 'https://api.wordpress.org/plugins/info/1.2/' ) ;
579+ infoUrl . searchParams . set ( 'action' , 'plugin_information' ) ;
580+ infoUrl . searchParams . set ( 'request[slug]' , entry . slug ) ;
581+ const infoResponse = await fetcher ( infoUrl , { redirect : 'error' } ) ;
582+ if ( ! infoResponse . ok || ! / ^ a p p l i c a t i o n \/ j s o n \b / i. test ( infoResponse . headers . get ( 'content-type' ) || '' ) ) throw new Error ( `WordPress.org plugin info failed for ${ entry . slug } .` ) ;
583+ const info = await infoResponse . json ( ) ;
584+ const version = String ( info ?. version || '' ) ;
585+ const source = String ( info ?. download_link || '' ) ;
586+ const url = new URL ( source ) ;
587+ if ( ! / ^ h t t p s : $ / . test ( url . protocol ) || url . hostname !== 'downloads.wordpress.org' || ! version || ! / ^ \d [ 0 - 9 A - Z a - z . _ + - ] * $ / . test ( version ) ) throw new Error ( `WordPress.org plugin info returned an invalid immutable package for ${ entry . slug } .` ) ;
588+ const cacheKey = `${ entry . slug } -${ version } ` ;
589+ const cachePath = path . join ( cacheDirectory , cacheKey , 'package.zip' ) ;
590+ fs . mkdirSync ( path . dirname ( cachePath ) , { recursive : true } ) ;
591+ let bytes ;
592+ if ( fs . existsSync ( cachePath ) ) bytes = fs . readFileSync ( cachePath ) ;
593+ else {
594+ const download = await fetcher ( url , { redirect : 'error' } ) ;
595+ const contentLength = Number ( download . headers . get ( 'content-length' ) || 0 ) ;
596+ if ( ! download . ok || ! / ^ a p p l i c a t i o n \/ ( z i p | o c t e t - s t r e a m ) \b / i. test ( download . headers . get ( 'content-type' ) || '' ) || ( contentLength && contentLength > 100 * 1024 * 1024 ) ) throw new Error ( `WordPress.org package download failed policy validation for ${ entry . slug } .` ) ;
597+ bytes = Buffer . from ( await download . arrayBuffer ( ) ) ;
598+ if ( ! bytes . length || bytes . length > 100 * 1024 * 1024 ) throw new Error ( `WordPress.org package exceeds host size policy for ${ entry . slug } .` ) ;
599+ fs . writeFileSync ( cachePath , bytes ) ;
600+ }
601+ const sha256 = createHash ( 'sha256' ) . update ( bytes ) . digest ( 'hex' ) ;
602+ entries . push ( { ...entry , host_resolution : { schema : 'static-site-importer/host-package-resolution/v1' , slug : entry . slug , version, source_url : url . toString ( ) , archive_sha256 : sha256 , archive_path : cachePath } } ) ;
603+ }
604+ return { ...plan , entries } ;
605+ }
606+
607+ async function discoverFixtureDependencyPlan ( { fixtures, outputDirectory, staticSiteImporterPath, options, batchSuffix } ) {
608+ const plans = [ ] ;
609+ for ( const fixture of fixtures ) {
610+ const fixtureDirectory = path . join ( outputDirectory , fixture . id ) ;
611+ const runtimeDirectory = `/wordpress/wp-content/uploads/static-site-importer-fixture-matrix/${ fixture . id } ` ;
612+ const planName = `dependency-plan-${ batchSuffix } .json` ;
613+ const artifactsDir = path . join ( outputDirectory , 'dependency-discovery' , `${ batchSuffix } -${ fixture . id } ` ) ;
614+ const recipeFile = path . join ( artifactsDir , 'recipe.json' ) ;
615+ const outputFile = path . join ( artifactsDir , 'output.json' ) ;
616+ fs . mkdirSync ( artifactsDir , { recursive : true } ) ;
617+ const recipe = {
618+ schema : 'wp-codebox/workspace-recipe/v1' ,
619+ runtime : { wp : options . wordpressVersion || 'latest' , blueprint : { } } ,
620+ inputs : {
621+ stagedFiles : [ { source : path . join ( fixtureDirectory , 'artifact.json' ) , target : path . join ( runtimeDirectory , 'artifact.json' ) } ] ,
622+ extra_plugins : [ { source : staticSiteImporterPath , slug : options . staticSiteImporterSlug || 'static-site-importer' , activate : true } ] ,
623+ } ,
624+ workflow : { steps : [
625+ { command : 'wordpress.wp-cli' , args : [ `command=plugin activate ${ ( options . staticSiteImporterPlugin || 'static-site-importer/static-site-importer.php' ) } ` ] } ,
626+ { command : 'wordpress.wp-cli' , args : [ `command=static-site-importer plan-artifact-dependencies --artifact=${ path . join ( runtimeDirectory , 'artifact.json' ) } --slug=${ fixture . id } --name=${ JSON . stringify ( fixture . label ) } --output=${ path . join ( runtimeDirectory , planName ) } ` ] } ,
627+ ] } ,
628+ artifacts : { directory : artifactsDir , typed : [ { name : 'dependency-plan' , type : 'static-site-importer/runtime-dependency-plan' , path : path . join ( runtimeDirectory , planName ) , required : true , parseJson : true , contentType : 'application/json' , payloadSchema : 'static-site-importer/runtime-dependency-plan/v1' } ] } ,
629+ } ;
630+ writeJsonArtifact ( recipeFile , recipe ) ;
631+ const discovery = await runWpCodeboxRecipe ( { recipeFile, artifactsDir, outputFile, wpCodeboxBin : options . wpCodeboxBin , inactivityTimeoutMs : batchInactivityTimeoutMs ( options ) } ) ;
632+ const plan = findDependencyPlan ( artifactsDir ) ;
633+ if ( ! plan ) throw new Error ( `Dependency discovery did not persist a valid plan for fixture ${ fixture . id } .` ) ;
634+ // Discovery only mounts SSI. Provider packages are resolved and activated by
635+ // the final recipe's extra_plugins setup, before its workflow begins; asking
636+ // this runtime for a provider receipt would incorrectly require Jetpack/Woo
637+ // to be installed during planning.
638+ plans . push ( plan ) ;
639+ }
640+ const entries = new Map ( ) ;
641+ for ( let index = 0 ; index < plans . length ; index += 1 ) {
642+ const fixtureId = fixtures [ index ] . id ;
643+ for ( const entry of plans [ index ] . entries ) {
644+ const key = `${ entry . source_kind } :${ entry . slug } :${ entry . plugin_entrypoint } ` ;
645+ const existing = entries . get ( key ) ;
646+ entries . set ( key , {
647+ ...( existing || entry ) ,
648+ fixture_ids : [ ...new Set ( [ ...( existing ?. fixture_ids || [ ] ) , fixtureId ] ) ] . sort ( ) ,
649+ } ) ;
650+ }
651+ }
652+ return { schema : 'static-site-importer/runtime-dependency-plan/v1' , artifact_sha256 : plans . map ( ( plan ) => plan . artifact_sha256 ) . sort ( ) . join ( ',' ) , entries : [ ...entries . values ( ) ] } ;
653+ }
654+
655+ function findDependencyPlan ( directory ) {
656+ const visit = ( current ) => {
657+ for ( const entry of fs . readdirSync ( current , { withFileTypes : true } ) ) {
658+ const child = path . join ( current , entry . name ) ;
659+ if ( entry . isDirectory ( ) ) {
660+ const found = visit ( child ) ;
661+ if ( found ) return found ;
662+ } else if ( entry . isFile ( ) && entry . name . endsWith ( '.json' ) ) {
663+ const parsed = parseJsonText ( fs . readFileSync ( child , 'utf8' ) ) ;
664+ if ( parsed ?. schema === 'static-site-importer/runtime-dependency-plan/v1' && Array . isArray ( parsed . entries ) ) return parsed ;
665+ }
666+ }
667+ return null ;
668+ } ;
669+ return visit ( directory ) ;
670+ }
671+
563672function createFixtureMatrixProgress ( matrix , options ) {
564673 const complete = new Set ( ) ;
565674 const write = typeof options . progress === 'function'
0 commit comments