char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
d1201e2
main
1import { assert , assertEquals , assertRejects , assertStringIncludes } from "@std/assert" ; 2import { basename , dirname , join } from "node:path" ; 3import { HOST_GLOBAL } from "@berry/api/host.ts" ; 4import { extensionWeb } from "./web.ts" ; 5import { tempDir } from "@berry/util" ; 6 7async function extension ( files :Record < string , string >, run :( dir :string ) => Promise < void >) { 8await usingtemporary = await tempDir ({ prefix :"berry-web-" }); 9const dir = temporary . path ; 10for ( const [ name , text ] of Object . entries ( files )) { 11await Deno . mkdir ( dirname ( join ( dir , name )), { recursive :true }); 12await Deno . writeTextFile ( join ( dir , name ), text ); 13} 14await run ( dir ); 15} 16 17const importBuilt = ( js :string ) => 18import ( `data:text/javascript;base64, ${ btoa ( unescape ( encodeURIComponent ( js )))} ` ); 19 20Deno . test ( "a frontend cache survives a new builder and invalidates imported inputs" , () => 21extension ({ 22"source/web.tsx" :`import value from "../shared.ts"; import "./web.css"; 23import asset from "./large.wasm?url"; export default { value, asset };` , 24"shared.ts" :`export default "first";` , 25"source/web.css" :".probe { color: red }" , 26"source/large.wasm" :"\0asm" . padEnd ( 64 * 1024 , "x" ), 27"source/mod.ts" :"export default 1;" , 28"source/web.test.ts" :"export default 1;" , 29}, async ( dir ) => { 30await usingcache = await tempDir (); 31const sources :string [] = []; 32const fresh = () => extensionWeb ({ cacheDirectory :cache . path }); 33const build = ( web = fresh ()) => 34web . build ( "probe" , entry , ( source ) => { 35sources . push ( source ); 36}); 37const entry = join ( dir , "source/web.tsx" ); 38await usingapi = await tempDir ({ dir :join ( Deno . cwd (), "api" ), prefix :".web-cache-test-" }); 39const unrelatedApi = join ( api . path , "unrelated.ts" ); 40await Deno . writeTextFile ( unrelatedApi , "export default 1;" ); 41const importedApi = join ( api . path , "shared.ts" ); 42await Deno . writeTextFile ( importedApi , `export default "first";` ); 43await Deno . writeTextFile ( 44join ( dir , "shared.ts" ), 45`export { default } from "@berry/api/ ${ basename ( api . path )} /shared.ts";` , 46); 47const web = fresh (); 48const first = await build ( web ); 49const restarted = fresh (); 50assertEquals ( await build ( restarted ), first ); 51assertEquals ( 52await restarted . serve ( first . module ) ! . text (), 53await web . serve ( first . module ) ! . text (), 54); 55assertEquals ( 56await restarted . serve ( first . styles [ 0 ]) ! . text (), 57await web . serve ( first . styles [ 0 ]) ! . text (), 58); 59const js = await restarted . serve ( first . module ) ! . text (); 60const asset = js . match ( / new URL\("([^"]+)", import\.meta\.url\) / ) ! [ 1 ]; 61const path = new URL ( asset , `http://berry ${ first . module } ` ). pathname ; 62assertEquals ( await restarted . serve ( path ) ! . bytes (), await web . serve ( path ) ! . bytes ()); 63assertEquals ( sources , [ "build" , "cache" ]); 64 65await Deno . writeTextFile ( join ( dir , "source/mod.ts" ), "export default 2;" ); 66await Deno . writeTextFile ( join ( dir , "source/web.test.ts" ), "export default 2;" ); 67await Deno . writeTextFile ( unrelatedApi , "export default 2;" ); 68assertEquals ( await build (), first ); 69assertEquals ( sources . at ( - 1 ), "cache" ); 70 71await Deno . writeTextFile ( importedApi , `export default "api changed";` ); 72const apiChanged = fresh (); 73const apiBuilt = await build ( apiChanged ); 74assertStringIncludes ( await apiChanged . serve ( apiBuilt . module ) ! . text (), "api changed" ); 75assertEquals ( sources . at ( - 1 ), "build" ); 76 77const shared = join ( dir , "shared.ts" ); 78const mtime = Deno . statSync ( shared ). mtime ! ; 79await Deno . writeTextFile ( shared , `export default "other";` ); 80await Deno . utime ( shared , mtime , mtime ); 81const changed = fresh (); 82const second = await build ( changed ); 83assert ( second . module !== first . module ); 84assertStringIncludes ( await changed . serve ( second . module ) ! . text (), "other" ); 85assertEquals ( sources . at ( - 1 ), "build" ); 86 87await Deno . writeTextFile ( join ( dir , "source/web.css" ), ".probe { color: blue }" ); 88const third = await build (); 89assert ( third . styles [ 0 ] !== second . styles [ 0 ]); 90await Deno . writeTextFile ( join ( dir , "source/large.wasm" ), "\0asm" . padEnd ( 64 * 1024 , "y" )); 91assert (( await build ()). module !== third . module ); 92})); 93 94Deno . test ( "CSS imports and inlined URL assets invalidate frontend caches" , () => 95extension ({ 96"web.tsx" :`import "./web.css"; export default {};` , 97"web.css" :`@import "./colors.css"; .probe { background: url("./dot.svg") }` , 98"colors.css" :".probe { color: red }" , 99"dot.svg" :`<svg xmlns="http://www.w3.org/2000/svg"><circle r="1" fill="red"/></svg>` , 100}, async ( dir ) => { 101await usingcache = await tempDir (); 102const sources :string [] = []; 103const build = () => 104extensionWeb ({ cacheDirectory :cache . path }). build ( 105"probe" , 106join ( dir , "web.tsx" ), 107( source ) => { 108sources . push ( source ); 109}, 110); 111const first = await build (); 112assertEquals ( await build (), first ); 113assertEquals ( sources , [ "build" , "cache" ]); 114await Deno . writeTextFile ( join ( dir , "colors.css" ), ".probe { color: blue }" ); 115const second = await build (); 116assert ( second . styles [ 0 ] !== first . styles [ 0 ]); 117assertEquals ( sources . at ( - 1 ), "build" ); 118await Deno . writeTextFile ( 119join ( dir , "dot.svg" ), 120`<svg xmlns="http://www.w3.org/2000/svg"><circle r="1" fill="blue"/></svg>` , 121); 122assert (( await build ()). styles [ 0 ] !== second . styles [ 0 ]); 123assertEquals ( sources . at ( - 1 ), "build" ); 124})); 125 126Deno . test ( "a broken or unwritable frontend cache does not prevent a build" , () => 127extension ({ "web.tsx" :`export default "usable";` }, async ( dir ) => { 128await usingcache = await tempDir (); 129const entry = join ( dir , "web.tsx" ); 130const web = extensionWeb ({ cacheDirectory :cache . path }); 131const first = await web . build ( "probe" , entry ); 132for await ( const file of Deno . readDir ( cache . path )) { 133const path = join ( cache . path , file . name ); 134const text = await Deno . readTextFile ( path ); 135await Deno . writeTextFile ( path , text . replace ( "usable" , "broken" )); 136} 137const rebuilt = extensionWeb ({ cacheDirectory :cache . path }); 138assertEquals ( await rebuilt . build ( "probe" , entry ), first ); 139assertEquals ( await rebuilt . serve ( first . module ) ! . text (), await web . serve ( first . module ) ! . text ()); 140const blocked = join ( dir , "blocked" ); 141await Deno . writeTextFile ( blocked , "not a directory" ); 142const uncached = extensionWeb ({ cacheDirectory :blocked }); 143const built = await uncached . build ( "probe" , entry ); 144assertStringIncludes ( await uncached . serve ( built . module ) ! . text (), "usable" ); 145})); 146 147Deno . test ( "frontends compile out of process, while cache hits start no compiler" , () => 148extension ({ "web.tsx" :"export default 1;" }, async ( dir ) => { 149await usingcache = await tempDir (); 150const Command = Deno . Command ; 151let started = 0 ; 152Deno . Command = class extends Command { 153constructor ( ...args :ConstructorParameters < typeof Deno . Command >) { 154super ( ...args ); 155started ++ ; 156} 157}; 158try { 159const web = extensionWeb ({ cacheDirectory :cache . path }); 160const entry = join ( dir , "web.tsx" ); 161await web . build ( "probe" , entry ); 162assert ( started > 0 ); 163const previous = started ; 164await web . build ( "probe" , entry ); 165assertEquals ( started , previous ); 166} finally { 167Deno . Command = Command ; 168} 169})); 170 171Deno . test ( "a built frontend uses the app's instances of the shared modules" , () => 172extension ({ 173"web.tsx" :` 174import { Signal } from "@char/aftercare"; 175import * as api from "@berry/api/web.ts"; 176import "./web.css"; 177export default { Signal, api, view: <b>hi</b> }; 178` , 179"web.css" :".probe { color: red }" , 180}, async ( dir ) => { 181const web = extensionWeb (); 182const { module, styles} = await web . build ( "probe" , join ( dir , "web.tsx" )); 183const js = await web . serve ( module ) ! . text (); 184assert ( ! js . includes ( `from "@char/aftercare"` ), "no import of its own copy" ); 185 186class Signal {} 187const api = { code () {} }; 188( globalThis as Record < string , unknown >)[ HOST_GLOBAL ] = { 189"@char/aftercare" :{ Signal}, 190"@char/aftercare/jsx-runtime" :{ 191jsx :( tag :string ) => `< ${ tag } >` , 192jsxs :( tag :string ) => `< ${ tag } >` , 193}, 194"@berry/api/web.ts" :api , 195}; 196try { 197const built = ( await importBuilt ( js )). default ; 198assert ( built . Signal === Signal ); 199assert ( built . api === api ); 200assertEquals ( built . view , "<b>" ); 201} finally { 202delete ( globalThis as Record < string , unknown >)[ HOST_GLOBAL ]; 203} 204 205assertEquals ( styles . length , 1 ); 206const css = web . serve ( styles [ 0 ]) ! ; 207assertEquals ( css . headers . get ( "content-type" ), "text/css; charset=utf-8" ); 208assertStringIncludes ( await css . text (), ".probe" ); 209})); 210 211Deno . test ( "a rebuild moves to a new url, and a failed one keeps the last" , () => 212extension ({ "web.tsx" :`export default 1;` }, async ( dir ) => { 213const web = extensionWeb (); 214const first = await web . build ( "probe" , join ( dir , "web.tsx" )); 215assertEquals ( first . styles , []); 216assertEquals ( 217web . serve ( first . module ) ! . headers . get ( "cache-control" ), 218"public, max-age=31536000, immutable" , 219); 220 221await Deno . writeTextFile ( join ( dir , "web.tsx" ), `export default 2;` ); 222const second = await web . build ( "probe" , join ( dir , "web.tsx" )); 223assert ( second . module !== first . module ); 224assertEquals ( web . serve ( first . module ), undefined ); 225 226await Deno . writeTextFile ( join ( dir , "web.tsx" ), `export default (;` ); 227await assertRejects (() => web . build ( "probe" , join ( dir , "web.tsx" )), Error , "web.tsx" ); 228assertStringIncludes ( await web . serve ( second . module ) ! . text (), "2" ); 229 230await Deno . writeTextFile ( join ( dir , "web.tsx" ), `export default 3;` ); 231const third = await web . build ( "probe" , join ( dir , "web.tsx" )); 232assertStringIncludes ( await web . serve ( third . module ) ! . text (), "3" ); 233})); 234 235Deno . test ( "a frontend's large files are served beside its module rather than inlined into it" , () => 236extension ({ 237"web.tsx" :`export default new URL("./big.wasm", import.meta.url).href;` , 238"big.wasm" :"\0asm" . padEnd ( 64 * 1024 , "x" ), 239}, async ( dir ) => { 240await usingcache = await tempDir (); 241const web = extensionWeb ({ cacheDirectory :cache . path }); 242const { module} = await web . build ( "probe" , join ( dir , "web.tsx" )); 243const js = await web . serve ( module ) ! . text (); 244assert ( js . length < 16 * 1024 , "the module only names it" ); 245const named = js . match ( / new URL\("([^"]+)", import\.meta\.url\) / ) ! [ 1 ]; 246const file = web . serve ( new URL ( named , `http://berry ${ module } ` ). pathname ) ! ; 247assertEquals ( file . headers . get ( "content-type" ), "application/wasm" ); 248assertEquals (( await file . bytes ()). length , 64 * 1024 ); 249await Deno . writeTextFile ( join ( dir , "big.wasm" ), "\0asm" . padEnd ( 64 * 1024 , "y" )); 250const changed = await extensionWeb ({ cacheDirectory :cache . path }). build ( 251"probe" , 252join ( dir , "web.tsx" ), 253); 254assert ( changed . module !== module ); 255})); 256 257Deno . test ( "a dynamic import is served beside the module and loaded only when imported" , () => 258extension ({ 259"web.tsx" :`export default { load: () => import("./heavy.ts").then((m) => m.heavy) };` , 260"heavy.ts" :`export const heavy = "heavy:" + "x".repeat(32 * 1024);` , 261}, async ( dir ) => { 262await usingcache = await tempDir (); 263const entry = join ( dir , "web.tsx" ); 264const first = await extensionWeb ({ cacheDirectory :cache . path }). build ( "probe" , entry ); 265const web = extensionWeb ({ cacheDirectory :cache . path }); 266let source ; 267const { module} = await web . build ( "probe" , entry , ( value ) => { 268source = value ; 269}); 270assertEquals ( source , "cache" ); 271assertEquals ( module , first . module ); 272assert (( await web . serve ( module ) ! . text ()). length < 16 * 1024 , "the module leaves it out" ); 273// imported from files, as a page imports it from the server, so that relative imports resolve beside it 274await usingcopy = await tempDir (); 275const served = async ( path :string ) => { 276const file = join ( copy . path , path ); 277await Deno . mkdir ( join ( file , ".." ), { recursive :true }); 278await Deno . writeTextFile ( file , await web . serve ( path ) ! . text ()); 279for ( const [, name ] of ( await Deno . readTextFile ( file )). matchAll ( / import\("\.\/([^"]+)"\) / g )) { 280await served ( join ( path , ".." , name )); 281} 282}; 283await served ( module ); 284const built = ( await import ( `file:// ${ join ( copy . path , module )} ` )). default ; 285assert (( await built . load ()). startsWith ( "heavy:" )); 286await Deno . writeTextFile ( join ( dir , "heavy.ts" ), `export const heavy = "changed";` ); 287const changed = await web . build ( "probe" , entry , ( value ) => { 288source = value ; 289}); 290assertEquals ( source , "build" ); 291assert ( changed . module !== module ); 292})); 293 294Deno . test ( "a change to a frontend's styles alone moves its stylesheet" , () => 295extension ({ 296"web.tsx" :`import "./web.css"; export default 1;` , 297"web.css" :".probe { color: red }" , 298}, async ( dir ) => { 299const web = extensionWeb (); 300const first = await web . build ( "probe" , join ( dir , "web.tsx" )); 301await Deno . writeTextFile ( join ( dir , "web.css" ), ".probe { color: blue }" ); 302const second = await web . build ( "probe" , join ( dir , "web.tsx" )); 303assert ( second . styles [ 0 ] !== first . styles [ 0 ]); 304assertStringIncludes ( await web . serve ( second . styles [ 0 ]) ! . text (), "blue" ); 305assertEquals ( web . serve ( first . styles [ 0 ]), undefined ); 306}));