char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
d1201e2
main
1// vite and its plugins live only as long as one frontend build subprocess. 2 3import { encodeBase64 } from "@std/encoding/base64" ; 4import { fromFileUrl , isAbsolute } from "@std/path" ; 5import aftercare from "@char/aftercare/vite" ; 6import deno from "@deno/vite-plugin" ; 7import { build , type Plugin , type Rolldown } from "vite" ; 8import { HOST_GLOBAL , HOST_MODULES } from "@berry/api/host.ts" ; 9 10export type FrontendBuild = { 11chunks :{ fileName :string ; code :string ; isEntry :boolean }[]; 12assets :{ fileName :string ; source :string ; binary :boolean }[]; 13dependencies :string []; 14}; 15 16const isHost = ( specifier :unknown ) :specifier isstring => 17( HOST_MODULES as readonly unknown []). includes ( specifier ); 18 19/** Imports of the host's modules become reads of the instances the app published, as `const { … } = host[…]`. */ 20function hostModules () :Plugin { 21return { 22name :"berry-host-modules" , 23renderChunk ( code ) { 24let rewritten = "" ; 25let at = 0 ; 26for ( const node of this . parse ( code ). body ) { 27if ( node . type !== "ImportDeclaration" || ! isHost ( node . source . value )) continue ; 28const host = `globalThis[ ${ JSON . stringify ( HOST_GLOBAL )} ][ ${ 29JSON . stringify ( node . source . value ) 30} ]` ; 31const bindings :string [] = []; 32const statements :string [] = []; 33for ( const specifier of node . specifiers ) { 34if ( specifier . type === "ImportNamespaceSpecifier" ) { 35statements . push ( `const ${ specifier . local . name } = ${ host } ;` ); 36continue ; 37} 38const imported = specifier . type === "ImportDefaultSpecifier" 39 ?"default" 40 :specifier . imported . type === "Identifier" 41 ?specifier . imported . name 42 :JSON . stringify ( specifier . imported . value ); 43bindings . push ( 44imported === specifier . local . name ?imported :` ${ imported } : ${ specifier . local . name } ` , 45); 46} 47if ( bindings . length > 0 ) statements . push ( `const { ${ bindings . join ( ", " )} } = ${ host } ;` ); 48// one line for one line, so the source map still lines up 49rewritten += code . slice ( at , node . start ) + statements . join ( " " ); 50at = node . end ; 51} 52return at === 0 ?null :{ code :rewritten + code . slice ( at ), map :null }; 53}, 54}; 55} 56 57async function buildFrontend ( entry :string ) :Promise < FrontendBuild > { 58const root = fromFileUrl ( new URL ( "../" , import . meta. url )); 59const dependencies = new Set ([ entry ]); 60function track < C extends Rolldown . PluginContext , A extends unknown [], R > ( 61hook :( this :C , ...args :A ) => R , 62) :( this :C , ...args :A ) => R { 63return function ( this :C , ...args :A ) :R { 64const context = new Proxy ( this , { 65get ( target , key ) { 66if ( key === "addWatchFile" ) { 67return ( path :string ) => { 68if ( isAbsolute ( path )) dependencies . add ( path ); 69target . addWatchFile ( path ); 70}; 71} 72const value = Reflect . get ( target , key , target ); 73return typeof value === "function" ?value . bind ( target ) :value ; 74}, 75}); 76return Reflect . apply ( hook , context , args ); 77}; 78} 79const result = await build ({ 80 root, 81configFile :false , 82mode :"production" , 83// a failure rejects with its message; the caller reports it 84logLevel :"silent" , 85publicDir :false , 86// assets resolve against the module that names them, beside it under /ext/<name>/ 87base :"./" , 88plugins :[ 89deno ({ 90onLoad ({ id}) { 91if ( id . startsWith ( "file:" )) dependencies . add ( fromFileUrl ( id )); 92}, 93}), 94aftercare (), 95hostModules (), 96{ 97name :"berry-build-inputs" , 98configResolved ( config ) { 99// vite hides rolldown's watched files, including css imports and worker dependencies 100for ( const plugin of config . plugins ) { 101const load = plugin . load ; 102if ( load ) { 103plugin . load = typeof load === "function" 104 ?track ( load ) 105 :{ ...load , handler :track ( load . handler ) }; 106} 107const transform = plugin . transform ; 108if ( transform ) { 109plugin . transform = typeof transform === "function" 110 ?track ( transform ) 111 :{ ...transform , handler :track ( transform . handler ) }; 112} 113} 114}, 115generateBundle () { 116for ( const id of this . getModuleIds ()) { 117const path = id . split ( "?" )[ 0 ]; 118if ( isAbsolute ( path )) dependencies . add ( path ); 119} 120}, 121}, 122], 123build :{ 124write :false , 125minify :false , 126sourcemap :"inline" , 127modulePreload :false , 128// not library mode, which inlines every asset however large, such as a wasm module 129rolldownOptions :{ 130input :entry , 131external :[ ...HOST_MODULES ], 132preserveEntrySignatures :"strict" , 133output :{ 134format :"es" , 135// a dynamic import is a chunk of its own, so an extension can leave heavy code until it needs it 136entryFileNames :"[name]-[hash].js" , 137chunkFileNames :"[name]-[hash].js" , 138assetFileNames :"[name]-[hash][extname]" , 139}, 140}, 141}, 142}); 143const outputs = ( Array . isArray ( result ) ?result :[ result as Rolldown . RolldownOutput ]) 144. flatMap (( r ) => r . output ); 145return { 146chunks :outputs . filter (( o ) :o isRolldown . OutputChunk => o . type === "chunk" ) 147. map (({ fileName, code, isEntry}) => ({ fileName, code, isEntry})), 148assets :outputs . filter (( o ) :o isRolldown . OutputAsset => o . type === "asset" ) 149. map (({ fileName, source}) => ({ 150 fileName, 151source :typeof source === "string" ?source :encodeBase64 ( source ), 152binary :typeof source !== "string" , 153})), 154dependencies :[ ...dependencies ], 155}; 156} 157 158if ( import . meta. main ) { 159try { 160console . log ( JSON . stringify ( await buildFrontend ( Deno . args [ 0 ]))); 161} catch ( error ) { 162console . error ( error instanceof Error ?error . message :String ( error )); 163Deno . exit ( 1 ); 164} 165}