@@ -782,6 +782,102 @@ test("e2e: framework adapters mount, update, unmount and fallback in browser", a
782782 }
783783} ) ;
784784
785+ test ( "e2e: renderPlanInBrowser covers auto-pin-latest and manifest-only modes" , async ( t ) => {
786+ const e2eTempRoot = path . join ( REPO_ROOT , ".tmp" ) ;
787+ await mkdir ( e2eTempRoot , { recursive : true } ) ;
788+ const tempDir = await mkdtemp (
789+ path . join ( e2eTempRoot , "renderify-e2e-autopin-manifest-modes-" ) ,
790+ ) ;
791+ const harnessSourcePath = path . join ( tempDir , "autopin-manifest-harness.ts" ) ;
792+ const harnessBundlePath = path . join ( tempDir , "autopin-manifest-harness.js" ) ;
793+ const port = await allocatePort ( ) ;
794+ const baseUrl = `http://127.0.0.1:${ port } ` ;
795+
796+ let browser : Browser | undefined ;
797+ let closeHarnessServer : ( ( ) => Promise < void > ) | undefined ;
798+
799+ try {
800+ const runtimeIndexPath = path
801+ . join ( REPO_ROOT , "packages" , "runtime" , "src" , "index.ts" )
802+ . replace ( / \\ / g, "/" ) ;
803+ const harnessSource = AUTOPIN_MANIFEST_HARNESS_SOURCE . replace (
804+ "__RUNTIME_INDEX_PATH__" ,
805+ runtimeIndexPath ,
806+ ) ;
807+
808+ await writeFile ( harnessSourcePath , harnessSource , "utf8" ) ;
809+
810+ const esbuildResult = await runCommand ( "pnpm" , [
811+ "exec" ,
812+ "esbuild" ,
813+ harnessSourcePath ,
814+ "--bundle" ,
815+ "--format=esm" ,
816+ "--platform=browser" ,
817+ "--target=es2022" ,
818+ `--outfile=${ harnessBundlePath } ` ,
819+ ] ) ;
820+ assert . equal ( esbuildResult . code , 0 , esbuildResult . stderr ) ;
821+
822+ const fsPromises = await import ( "node:fs/promises" ) ;
823+ const harnessBundle = await fsPromises . readFile ( harnessBundlePath , "utf8" ) ;
824+
825+ closeHarnessServer = await startAutoPinManifestHarnessServer ( {
826+ port,
827+ harnessBundle,
828+ } ) ;
829+
830+ try {
831+ browser = await chromium . launch ( { headless : true } ) ;
832+ } catch ( error ) {
833+ t . skip (
834+ `playwright chromium is unavailable: ${ error instanceof Error ? error . message : String ( error ) } ` ,
835+ ) ;
836+ return ;
837+ }
838+
839+ const page = await browser . newPage ( ) ;
840+ await page . goto ( baseUrl , {
841+ waitUntil : "networkidle" ,
842+ } ) ;
843+
844+ const report = ( await page . evaluate ( async ( ) => {
845+ // @ts -expect-error runtime-served test harness module
846+ const harness = await import ( "/auto-pin-harness.js" ) ;
847+ return await harness . runAutoPinManifestE2E ( window . location . origin ) ;
848+ } ) ) as {
849+ autoPinHtml : string ;
850+ autoPinDiagnostics : string [ ] ;
851+ autoPinSecuritySafe : boolean ;
852+ manifestOnlyHtml : string ;
853+ manifestOnlyDiagnostics : string [ ] ;
854+ manifestOnlySecuritySafe : boolean ;
855+ } ;
856+
857+ assert . match ( report . autoPinHtml , / T o d a y : \s * 1 9 7 0 - 0 1 - 0 1 / ) ;
858+ assert . doesNotMatch ( report . autoPinHtml , / f a l l b a c k r o o t / ) ;
859+ assert . equal (
860+ report . autoPinDiagnostics . includes ( "RUNTIME_MANIFEST_MISSING" ) ,
861+ false ,
862+ ) ;
863+ assert . equal ( report . autoPinSecuritySafe , true ) ;
864+
865+ assert . match ( report . manifestOnlyHtml , / T o d a y : \s * 1 9 7 0 - 0 1 - 0 1 / ) ;
866+ assert . doesNotMatch ( report . manifestOnlyHtml , / f a l l b a c k r o o t / ) ;
867+ assert . equal (
868+ report . manifestOnlyDiagnostics . includes ( "RUNTIME_MANIFEST_MISSING" ) ,
869+ false ,
870+ ) ;
871+ assert . equal ( report . manifestOnlySecuritySafe , true ) ;
872+ } finally {
873+ await browser ?. close ( ) ;
874+ if ( closeHarnessServer ) {
875+ await closeHarnessServer ( ) ;
876+ }
877+ await rm ( tempDir , { recursive : true , force : true } ) ;
878+ }
879+ } ) ;
880+
785881function toBase64Url ( value : string ) : string {
786882 return Buffer . from ( value , "utf8" ) . toString ( "base64url" ) ;
787883}
@@ -1240,6 +1336,229 @@ async function startFrameworkAdapterHarnessServer(input: {
12401336 return ( ) => closeServer ( server ) ;
12411337}
12421338
1339+ async function startAutoPinManifestHarnessServer ( input : {
1340+ port : number ;
1341+ harnessBundle : string ;
1342+ } ) : Promise < ( ) => Promise < void > > {
1343+ const html = [
1344+ "<!doctype html>" ,
1345+ "<html>" ,
1346+ " <head>" ,
1347+ ' <meta charset="utf-8" />' ,
1348+ " <title>Renderify AutoPin Manifest E2E</title>" ,
1349+ " </head>" ,
1350+ " <body>" ,
1351+ ' <main id="app"></main>' ,
1352+ " </body>" ,
1353+ "</html>" ,
1354+ ] . join ( "\n" ) ;
1355+
1356+ const formatModuleSource = [
1357+ "export function format(input, pattern = 'yyyy-MM-dd') {" ,
1358+ " const value = input instanceof Date ? input : new Date(input);" ,
1359+ " if (pattern !== 'yyyy-MM-dd') {" ,
1360+ " throw new Error('unsupported format pattern: ' + pattern);" ,
1361+ " }" ,
1362+ " return value.toISOString().slice(0, 10);" ,
1363+ "}" ,
1364+ ] . join ( "\n" ) ;
1365+
1366+ const packageJsonSource = JSON . stringify ( {
1367+ name : "date-fns" ,
1368+ version : "4.1.0" ,
1369+ exports : {
1370+ "." : {
1371+ import : {
1372+ default : "./index.js" ,
1373+ } ,
1374+ } ,
1375+ "./format" : {
1376+ import : {
1377+ default : "./format.js" ,
1378+ } ,
1379+ } ,
1380+ } ,
1381+ } ) ;
1382+
1383+ const server = createServer ( ( req , res ) => {
1384+ const url = new URL ( req . url ?? "/" , "http://127.0.0.1" ) ;
1385+
1386+ if ( req . method === "GET" && url . pathname === "/" ) {
1387+ const body = Buffer . from ( html , "utf8" ) ;
1388+ res . statusCode = 200 ;
1389+ res . setHeader ( "content-type" , "text/html; charset=utf-8" ) ;
1390+ res . setHeader ( "content-length" , body . length ) ;
1391+ res . end ( body ) ;
1392+ return ;
1393+ }
1394+
1395+ if ( req . method === "GET" && url . pathname === "/auto-pin-harness.js" ) {
1396+ const body = Buffer . from ( input . harnessBundle , "utf8" ) ;
1397+ res . statusCode = 200 ;
1398+ res . setHeader ( "content-type" , "text/javascript; charset=utf-8" ) ;
1399+ res . setHeader ( "content-length" , body . length ) ;
1400+ res . end ( body ) ;
1401+ return ;
1402+ }
1403+
1404+ if ( req . method === "GET" && url . pathname === "/npm:date-fns" ) {
1405+ const body = Buffer . from ( "4.1.0" , "utf8" ) ;
1406+ res . statusCode = 200 ;
1407+ res . setHeader ( "content-type" , "text/plain; charset=utf-8" ) ;
1408+ res . setHeader ( "content-length" , body . length ) ;
1409+ res . end ( body ) ;
1410+ return ;
1411+ }
1412+
1413+ if (
1414+ req . method === "GET" &&
1415+ url . pathname === "/npm:date-fns@4.1.0/package.json"
1416+ ) {
1417+ const body = Buffer . from ( packageJsonSource , "utf8" ) ;
1418+ res . statusCode = 200 ;
1419+ res . setHeader ( "content-type" , "application/json; charset=utf-8" ) ;
1420+ res . setHeader ( "content-length" , body . length ) ;
1421+ res . end ( body ) ;
1422+ return ;
1423+ }
1424+
1425+ if (
1426+ req . method === "GET" &&
1427+ url . pathname === "/npm:date-fns@4.1.0/format.js"
1428+ ) {
1429+ const body = Buffer . from ( formatModuleSource , "utf8" ) ;
1430+ res . statusCode = 200 ;
1431+ res . setHeader ( "content-type" , "text/javascript; charset=utf-8" ) ;
1432+ res . setHeader ( "content-length" , body . length ) ;
1433+ res . end ( body ) ;
1434+ return ;
1435+ }
1436+
1437+ const notFound = Buffer . from ( "not found" , "utf8" ) ;
1438+ res . statusCode = 404 ;
1439+ res . setHeader ( "content-type" , "text/plain; charset=utf-8" ) ;
1440+ res . setHeader ( "content-length" , notFound . length ) ;
1441+ res . end ( notFound ) ;
1442+ } ) ;
1443+
1444+ await new Promise < void > ( ( resolve , reject ) => {
1445+ server . once ( "error" , reject ) ;
1446+ server . listen ( input . port , "127.0.0.1" , ( ) => {
1447+ server . off ( "error" , reject ) ;
1448+ resolve ( ) ;
1449+ } ) ;
1450+ } ) ;
1451+
1452+ return ( ) => closeServer ( server ) ;
1453+ }
1454+
1455+ const AUTOPIN_MANIFEST_HARNESS_SOURCE = `
1456+ import { JspmModuleLoader, renderPlanInBrowser } from "__RUNTIME_INDEX_PATH__";
1457+
1458+ const mountRoot = (id) => {
1459+ const existing = document.getElementById(id);
1460+ if (existing) {
1461+ existing.remove();
1462+ }
1463+
1464+ const container = document.createElement("section");
1465+ container.id = id;
1466+ document.body.appendChild(container);
1467+ return container;
1468+ };
1469+
1470+ const createPlan = (id, manifest) => ({
1471+ specVersion: "runtime-plan/v1",
1472+ id,
1473+ version: 1,
1474+ capabilities: {
1475+ domWrite: true,
1476+ maxExecutionMs: 8000,
1477+ },
1478+ root: {
1479+ type: "element",
1480+ tag: "section",
1481+ children: [{ type: "text", value: "fallback root" }],
1482+ },
1483+ ...(manifest ? { moduleManifest: manifest } : {}),
1484+ source: {
1485+ language: "js",
1486+ runtime: "renderify",
1487+ code: [
1488+ 'import { format } from "date-fns/format";',
1489+ "export default function App() {",
1490+ " return {",
1491+ " type: 'element',",
1492+ " tag: 'section',",
1493+ " children: [",
1494+ " { type: 'text', value: 'Today: ' + format(new Date(0), 'yyyy-MM-dd') },",
1495+ " ],",
1496+ " };",
1497+ "}",
1498+ ].join("\\n"),
1499+ },
1500+ });
1501+
1502+ export async function runAutoPinManifestE2E(baseUrl) {
1503+ const autoPinMount = mountRoot("auto-pin-mode");
1504+ const autoPinLoader = new JspmModuleLoader({
1505+ cdnBaseUrl: baseUrl,
1506+ });
1507+ const autoPinResult = await renderPlanInBrowser(
1508+ createPlan("auto_pin_latest_default"),
1509+ {
1510+ target: autoPinMount,
1511+ autoPinModuleLoader: autoPinLoader,
1512+ autoPinFetchTimeoutMs: 4000,
1513+ securityInitialization: { profile: "relaxed" },
1514+ runtimeOptions: {
1515+ moduleLoader: autoPinLoader,
1516+ remoteFallbackCdnBases: [],
1517+ remoteFetchTimeoutMs: 4000,
1518+ remoteFetchRetries: 0,
1519+ remoteFetchBackoffMs: 10,
1520+ },
1521+ },
1522+ );
1523+
1524+ const manifestOnlyMount = mountRoot("manifest-only-mode");
1525+ const manifestOnlyLoader = new JspmModuleLoader({
1526+ cdnBaseUrl: baseUrl,
1527+ });
1528+ const manifestOnlyResult = await renderPlanInBrowser(
1529+ createPlan("manifest_only_mode", {
1530+ "date-fns/format": {
1531+ resolvedUrl: baseUrl + "/npm:date-fns@4.1.0/format.js",
1532+ version: "4.1.0",
1533+ },
1534+ }),
1535+ {
1536+ target: manifestOnlyMount,
1537+ autoPinLatestModuleManifest: false,
1538+ securityInitialization: { profile: "relaxed" },
1539+ runtimeOptions: {
1540+ moduleLoader: manifestOnlyLoader,
1541+ remoteFallbackCdnBases: [],
1542+ remoteFetchTimeoutMs: 4000,
1543+ remoteFetchRetries: 0,
1544+ remoteFetchBackoffMs: 10,
1545+ },
1546+ },
1547+ );
1548+
1549+ return {
1550+ autoPinHtml: autoPinMount.innerHTML,
1551+ autoPinDiagnostics: autoPinResult.execution.diagnostics.map((item) => item.code),
1552+ autoPinSecuritySafe: autoPinResult.security.safe,
1553+ manifestOnlyHtml: manifestOnlyMount.innerHTML,
1554+ manifestOnlyDiagnostics: manifestOnlyResult.execution.diagnostics.map(
1555+ (item) => item.code,
1556+ ),
1557+ manifestOnlySecuritySafe: manifestOnlyResult.security.safe,
1558+ };
1559+ }
1560+ ` ;
1561+
12431562const FRAMEWORK_ADAPTERS_HARNESS_SOURCE = `
12441563import { h, render } from "preact";
12451564import {
0 commit comments