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