local-module-probe.mjs (8884B)
1 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 2 const equal=(a,b,message)=>assert(JSON.stringify(a)===JSON.stringify(b),message+': '+JSON.stringify(a)); 3 async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(String(e)),'Wrong rejection: '+e);return;}throw Error('Expected rejection');} 4 const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms)); 5 async function wait(fn){for(let i=0;i<4500;i++){if(fn())return;await delay(20);}throw Error('Timed out: '+el('status').textContent+' / '+el('compiled-module-status').textContent);} 6 const status=s=>wait(()=>el('status').textContent===s); 7 const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));}; 8 const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));}; 9 export async function run(control,backend){ 10 const base=new URL('../',document.querySelector('script[type=module][src]').src); 11 const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession; 12 const {validateLocalModule}=(await import(new URL('dist/probe-api.js',base))).localModules; 13 const {encodeModule}=(await import(new URL('dist/probe-api.js',base))).moduleBundle; 14 const {decodeBundle}=(await import(new URL('dist/probe-api.js',base))).bundle; 15 const index=await(await fetch(new URL('packages.json',base))).json(); 16 const math=index.releases.find(r=>r.name==='tricu-lib-native-math'&&r.version==='0.1.0'); 17 const source=`!import ${math.manifest} as N\n!export echo\n!export plus\n!export value\nprivate = 9\necho x = x\nplus x = N.add x private\nvalue = 42\nq x = x x\nhidden = q q`; 18 const host={before:async()=>{},after:()=>{},read:async()=>'',write:()=>{},running:()=>{}}; 19 const s=await createRuntimeSession(backend,true);let exported; 20 try{ 21 await s.compile(source); 22 exported=await validateLocalModule(await s.exportModule()); 23 equal(exported.names,['echo','plus','value'],'Only public exports, no imports/private divergent value'); 24 equal((await s.runExpression('N.add value private','number','pure',host)).value,'51','Export preserves expression scope'); 25 const start=(await(await fetch(control+'/requests')).json()).length; 26 await s.compile(`!import ${exported.manifest} as L\n!export answer\nanswer = L.plus 33`,undefined,[exported]); 27 equal((await s.runExpression('L.echo answer','number','pure',host)).value,'42','Local qualified import'); 28 await s.compile(`!import ${exported.manifest} *\n!export answer\nanswer = plus 33`,undefined,[exported]); 29 equal((await s.run('answer',[],'number','pure',host)).value,'42','Local open import'); 30 await s.compile(`!export * from ${exported.manifest}`,undefined,[exported]); 31 equal((await validateLocalModule(await s.exportModule())).manifest,exported.manifest,'Full reexport preserves native identity'); 32 const requests=(await(await fetch(control+'/requests')).json()).slice(start); 33 assert(!requests.some(p=>p.endsWith('/packages.json')||p.startsWith('/_arboricx/')),'Local resolution performs no registry requests'); 34 const corrupt=exported.bytes.slice();corrupt[0]^=1; 35 await rejects(()=>s.compile(`!import ${exported.manifest} *\n!export value`,undefined,[{...exported,bytes:corrupt}]),/magic/); 36 await rejects(()=>s.exportModule(),/No compiled module/); 37 const bindings=['x0 = t t t'];for(let i=1;i<=30;i++)bindings.push(`x${i} = t x${i-1} x${i-1}`); 38 await s.compile('!export deep\n!export alias\ndeep = (let\n '+bindings.join('\n ')+'\n in x30)\nalias = deep'); 39 const shared=decodeBundle(await s.exportModule()); 40 assert(shared.nodes.length<40,'Export preserves exponential sharing'); 41 equal(shared.exports[0].root,shared.exports[1].root,'Cross-export sharing preserved'); 42 const original=decodeBundle(exported.bytes),names=Array.from({length:24},(_,i)=>'entry'+i); 43 const wide=await validateLocalModule(encodeModule({...original,exports:names.map(name=>({...original.exports[0],name}))})); 44 equal(await s.compile(`!export * from ${wide.manifest}`,undefined,[wide]),names,'Local public interface is not capped at 16 definitions'); 45 equal((await validateLocalModule(await s.exportModule())).manifest,wide.manifest,'All 24 reexports survive portable export'); 46 47 await s.compile('!export action\naction = t "tricuIO" (t 1 (t 20 "file"))'); 48 const capability=await validateLocalModule(await s.exportModule()); 49 await s.compile(`!import ${capability.manifest} as L\n!export main\nmain = L.action`,undefined,[capability]); 50 await rejects(()=>s.run('main',[],'string','io',host),/Unsupported|unsupported/); 51 52 }finally{s.dispose();} 53 await status('Ready');change('runtime-backend',backend);await status('Ready'); 54 document.querySelector('[data-compiler-example=module]').click();await status('Compiled'); 55 input('source-editor',source);el('compile').click();await status('Compiled'); 56 const createURL=URL.createObjectURL,click=HTMLAnchorElement.prototype.click;let downloaded,name; 57 URL.createObjectURL=blob=>{downloaded=blob;return createURL(blob);};HTMLAnchorElement.prototype.click=function(){name=this.download;}; 58 try{ 59 el('export-compiled-module').click();await wait(()=>downloaded&&el('status').textContent==='Compiled'); 60 }finally{URL.createObjectURL=createURL;HTMLAnchorElement.prototype.click=click;} 61 const file=await validateLocalModule(new Uint8Array(await downloaded.arrayBuffer())); 62 equal(file.manifest,exported.manifest,'Actual-page download contains compiled public interface'); 63 equal(name,file.hash+'.arboricx','Download named by bundle hash'); 64 const select=bytes=>{const transfer=new DataTransfer();transfer.items.add(new File([bytes],'<svg onload=bad>.arboricx'));el('local-module-file').files=transfer.files;el('local-module-file').dispatchEvent(new Event('change'));}; 65 select(file.bytes);await wait(()=>el('local-module-list').children.length===1&&!el('local-module-file').disabled); 66 assert(!el('local-module-list').querySelector('svg'),'Filename renders as text'); 67 assert(el('local-module-list').textContent.includes(file.manifest),'Visible manifest import statement'); 68 await status('Compiled'); 69 select(file.bytes);await wait(()=>el('local-module-status').textContent.includes('already available')); 70 equal(el('local-module-list').children.length,1,'Duplicates are idempotent'); 71 select(new Uint8Array([0]));await wait(()=>el('local-module-status').textContent.includes('Truncated')); 72 equal(el('local-module-list').children.length,1,'Invalid files preserve collection'); 73 select(file.bytes);el('cancel-local-module').click();await wait(()=>el('local-module-status').textContent.includes('cancelled')); 74 equal(el('local-module-list').children.length,1,'Cancelling validation preserves collection'); 75 const consumer=`!import ${file.manifest} as Mine\n!export answer\nanswer = Mine.plus 33`; 76 input('source-editor',consumer);el('compile').click();await status('Compiled'); 77 input('expression-editor','Mine.echo answer');change('output-type','number');el('run').click();await status('Finished'); 78 equal(el('result').textContent,'42','File selection makes module importable'); 79 // Stop an actually divergent export, then rebuild using page-owned local bytes. 80 input('source-editor','!export bad\nq x = x x\nbad = q q');el('compile').click();await status('Compiled'); 81 el('export-compiled-module').click();await status('Exporting module');await delay(150);el('stop').click();await status('Stopped'); 82 assert(el('export-compiled-module').disabled,'Stop invalidates exportable draft'); 83 input('source-editor',consumer);el('compile').click();await status('Compiled'); 84 change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm'); 85 el('compile').click();await status('Compiled');input('expression-editor','answer');el('run').click();await status('Finished'); 86 equal(el('result').textContent,'42','Local bytes survive Stop and backend replacement'); 87 const frame=document.createElement('iframe');frame.src=new URL('index.html?noProbe=1',location.href).href;document.body.append(frame); 88 await wait(()=>frame.contentDocument?.getElementById('status')?.textContent==='Ready'); 89 equal(frame.contentDocument.getElementById('local-module-list').children.length,0,'A new page has no persisted local modules');frame.remove(); 90 [...el('local-module-list').querySelectorAll('button')].find(b=>b.textContent==='Remove').click(); 91 await status('Compilation failed');equal(el('local-module-list').children.length,0,'Removal clears module'); 92 assert(el('run').disabled&&el('export-compiled-module').disabled,'Removed dependency cannot use a stale compiled scope'); 93 return {userAgent:navigator.userAgent,source,manifest:exported.manifest,bytes:[...exported.bytes],checks:['local file validation; offline local-first imports; public portable export/reexport; shared DAGs; no private divergence; actual export/file import; duplicates/corruption; Stop/backend retention; page-only lifetime; removal invalidation']}; 94 }