@live-codes/odin-wasm 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Copies the assets that ship inside this package into a directory you serve.
3
+ //
4
+ // This is the browser story: a page cannot read a file inside node_modules, so
5
+ // the assets have to be published by whatever serves the page. One command drops
6
+ // a complete, self-describing copy into your public directory; point `baseUrl`
7
+ // at it and nothing else has to be hosted.
8
+ import { cp, mkdir, readdir, writeFile } from 'node:fs/promises';
9
+ import { resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { ASSET_RECEIPTS } from '../src/asset-receipts.js';
12
+
13
+ const ASSETS = fileURLToPath(new URL('../assets/', import.meta.url));
14
+
15
+ const USAGE = `Copy the assets that ship with this package into a directory you serve.
16
+
17
+ odin-wasm-copy-assets [directory]
18
+
19
+ directory where to write them (default: ./odin)
20
+
21
+ --print-path print the packaged assets directory and exit
22
+ --help print this
23
+ `;
24
+
25
+ const args = process.argv.slice(2);
26
+ if (args.includes('--help') || args.includes('-h')) {
27
+ console.log(USAGE);
28
+ process.exit(0);
29
+ }
30
+ if (args.includes('--print-path')) {
31
+ console.log(ASSETS);
32
+ process.exit(0);
33
+ }
34
+
35
+ const target = resolve(args.find((arg) => !arg.startsWith('-')) ?? 'odin');
36
+
37
+ await mkdir(target, { recursive: true });
38
+ for (const entry of await readdir(ASSETS, { withFileTypes: true })) {
39
+ await cp(resolve(ASSETS, entry.name), resolve(target, entry.name), { recursive: true, force: true });
40
+ }
41
+
42
+ // The copy carries the receipts for its own bytes, so whoever serves it can check it.
43
+ await writeFile(resolve(target, 'asset-receipts.json'), `${JSON.stringify(ASSET_RECEIPTS, null, 2)}\n`);
44
+
45
+ console.log(`Assets copied to ${target}`);
46
+ console.log('Serve that directory and pass its URL as baseUrl, for example:');
47
+ console.log(" await createCompiler({ baseUrl: new URL('/odin/', location.href) });");
@@ -0,0 +1,6 @@
1
+ var odinWasm=(()=>{var W=Object.defineProperty;var gt=Object.getOwnPropertyDescriptor;var mt=Object.getOwnPropertyNames;var St=Object.prototype.hasOwnProperty;var wt=(t,e)=>{for(var n in e)W(t,n,{get:e[n],enumerable:!0})},_t=(t,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let f of mt(e))!St.call(t,f)&&f!==n&&W(t,f,{get:()=>e[f],enumerable:!(a=gt(e,f))||a.enumerable});return t};var yt=t=>_t(W({},"__esModule",{value:!0}),t);var $t={};wt($t,{TARGETS:()=>Yt,createCompiler:()=>Lt});var V=Object.freeze({"odin.wasm":Object.freeze({bytes:3763439,sha256:"5044e383aa2415927cd518a4d3e6fdbcc6c99154ac90d212e154f18c092deef3"}),"odin-src.pack.gz":Object.freeze({bytes:2502169,sha256:"567b1c0c1b50d3e557d119927ade3373dda91364acae3302f2cf01950a8dd8f9"}),"odin.js":Object.freeze({bytes:72772,sha256:"e918ba1614af23f02299976122d5edf4c994873ce4c6cdab621d42f3e9cb9733"})});var Gt=Object.freeze(Object.keys(V)),Ut="baseUrl is required here. The assets that ship in this package can only be read where there is a filesystem, and a browser cannot reach a file inside an npm package \u2014 copy them somewhere your page can fetch with `npx --package @live-codes/odin-wasm odin-wasm-copy-assets <dir>` and pass that directory as baseUrl.";function et(t,e){if(t.baseUrl!=null&&t.baseUrl!=="")return At(t);if(!e)throw new Error(Ut);return Ct(e)}function At(t){let e=Tt(t.baseUrl);return{kind:"hosted",key:`hosted\0${e}`,description:e,baseUrl:e,async readAsset(n,a){let f=new URL(n,e),h=await fetch(f);if(!h.ok)throw new Error(`Failed to load the runtime asset ${f}: ${h.status}`);return rt(n,await bt(h,a))}}}function Ct(t){return{kind:"packaged",key:`packaged\0${t.root.href}`,description:`the assets packaged with this library (${t.root.href})`,async readAsset(e){let n;try{n=await t.readFile(e)}catch(a){throw new Error(`Failed to read the packaged asset ${e} from ${t.root.href}: ${a.message}`,{cause:a})}return rt(e,n)}}}function Tt(t){let e;try{e=t instanceof URL?t:new URL(t,globalThis.document?.baseURI)}catch(n){throw new Error(`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: ${n.message}`,{cause:n})}if(e.protocol!=="http:"&&e.protocol!=="https:")throw new Error(`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: got "${t}"`);return e.href.endsWith("/")?e.href:`${e.href}/`}async function bt(t,e){if(!e||!t.body)return new Uint8Array(await t.arrayBuffer());let n=Number(t.headers.get("content-length"))||0,a=t.body.getReader(),f=[],h=0;for(e(0,n);;){let{done:c,value:g}=await a.read();if(c)break;f.push(g),h+=g.length,e(h,n)}let m=new Uint8Array(h),y=0;for(let c of f)m.set(c,y),y+=c.length;return m}async function rt(t,e){let n=V[t];if(!n)throw new Error(`No pinned receipt for the runtime asset ${t}`);if(e.byteLength!==n.bytes)throw new Error(`The runtime asset ${t} is ${e.byteLength} bytes, expected ${n.bytes}`);let a=await xt(e);if(a!==n.sha256)throw new Error(`The runtime asset ${t} failed SHA-256 verification: expected ${n.sha256}, got ${a}`);return e}async function xt(t){let e=globalThis.crypto?.subtle;if(!e)throw new Error("Verifying the runtime assets needs crypto.subtle: a secure context in the browser, or Node 20 and later.");let n=await e.digest("SHA-256",t);return[...new Uint8Array(n)].map(a=>a.toString(16).padStart(2,"0")).join("")}async function nt(t){if(typeof DecompressionStream!="function")throw new Error("Inflating the standard library pack needs DecompressionStream");let e=new Blob([t]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(e).arrayBuffer())}var B=class extends Error{constructor(e){super(`WASI process exited with code ${e}`),this.code=e}},R=t=>{let e=[];for(let n of String(t).split("/"))if(!(n===""||n===".")){if(n===".."){e.pop();continue}e.push(n)}return e.join("/")},H=t=>{let e=t.lastIndexOf("/");return e<0?"":t.slice(0,e)},M=new TextEncoder,kt=new TextDecoder,v=class{constructor(){this.files=new Map,this.dirs=new Set([""])}mkdirAll(e){let n=R(e)===""?[]:R(e).split("/"),a="";for(let f of n)a=a===""?f:`${a}/${f}`,this.dirs.add(a)}writeFile(e,n){let a=R(e);this.mkdirAll(H(a));let f=n instanceof Uint8Array?n:new Uint8Array(n);this.files.set(a,{data:f,size:f.length})}readFile(e){let n=this.files.get(R(e));return n?n.data.subarray(0,n.size):null}exists(e){let n=R(e);return this.files.has(n)||this.dirs.has(n)}listDir(e){let n=R(e),a=n===""?"":`${n}/`,f=[];for(let h of this.dirs)h!==""&&h.startsWith(a)&&h.indexOf("/",a.length)<0&&h!==n&&f.push({name:h.slice(a.length),isDir:!0});for(let h of this.files.keys())h.startsWith(a)&&h.indexOf("/",a.length)<0&&f.push({name:h.slice(a.length),isDir:!1});return f.sort((h,m)=>h.name<m.name?-1:h.name>m.name?1:0),f}},Ft=t=>t instanceof Uint8Array?t:new Uint8Array(t),X=(t,e={})=>{let n=e.args??[],a=e.env??[],f=e.stdout??(()=>{}),h=e.stderr??(()=>{}),m=Ft(M.encode(e.stdin??"")),y=0,c=new Map,g=4;c.set(0,{kind:"stdin"}),c.set(1,{kind:"stdout"}),c.set(2,{kind:"stderr"}),c.set(3,{kind:"dir",path:""});let S=["","",""],u=null,E=()=>new Uint8Array(u.buffer),l=()=>new DataView(u.buffer),_=(r,o)=>kt.decode(E().subarray(r,r+o)),F=(r,o)=>{let s=S[r]+o,i=s.lastIndexOf(`
2
+ `);i>=0&&((r===1?f:h)(s.slice(0,i+1)),s=s.slice(i+1)),S[r]=s},O=()=>{for(let r of[1,2])S[r]!==""&&((r===1?f:h)(S[r]),S[r]="")},k=(r,o,s)=>{let i=l();i.setBigUint64(r+0,0n,!0),i.setBigUint64(r+8,0n,!0),i.setUint8(r+16,o),i.setBigUint64(r+24,1n,!0),i.setBigUint64(r+32,BigInt(s),!0),i.setBigUint64(r+40,0n,!0),i.setBigUint64(r+48,0n,!0),i.setBigUint64(r+56,0n,!0)},C=(r,o)=>{if(t.dirs.has(r))return k(o,3,0),0;let s=t.files.get(r);return s?(k(o,4,s.size),0):44},P=(r,o)=>{if(o<=r.data.length)return;let s=Math.max(r.data.length*2,4096);for(;s<o;)s*=2;let i=new Uint8Array(s);i.set(r.data.subarray(0,r.size)),r.data=i},D=(r,o,s,i)=>{let d=l(),p=E(),w=0;for(let A=0;A<s;A++){let T=d.getUint32(o+A*8,!0),U=d.getUint32(o+A*8+4,!0),b=Math.max(0,Math.min(U,r.size-i));if(p.set(r.data.subarray(i,i+b),T),i+=b,w+=b,b<U)break}return w},Z=(r,o,s,i)=>{let d=l(),p=E(),w=0;for(let A=0;A<s;A++){let T=d.getUint32(o+A*8,!0),U=d.getUint32(o+A*8+4,!0);P(r,i+U),r.data.set(p.subarray(T,T+U),i),i+=U,r.size=Math.max(r.size,i),w+=U}return w},tt=(r,o,s)=>{let i=l(),d=E(),p=s;for(let w=0;w<r.length;w++){let A=M.encode(r[w]);i.setUint32(o+w*4,p,!0),d.set(A,p),d[p+A.length]=0,p+=A.length+1}return 0},$=(r,o,s)=>{let i=c.get(r);if(!i||i.kind!=="dir")return null;let d=_(o,s);return R(d.startsWith("/")?d:`${i.path}/${d}`)};return{imports:{args_sizes_get(r,o){let s=l();return s.setUint32(r,n.length,!0),s.setUint32(o,n.reduce((i,d)=>i+M.encode(d).length+1,0),!0),0},args_get(r,o){return tt(n,r,o)},environ_sizes_get(r,o){let s=l();return s.setUint32(r,a.length,!0),s.setUint32(o,a.reduce((i,d)=>i+M.encode(d).length+1,0),!0),0},environ_get(r,o){return tt(a,r,o)},clock_res_get(r,o){return l().setBigUint64(o,1000n,!0),0},clock_time_get(r,o,s){let i=r===0?BigInt(Date.now())*1000000n:BigInt(Math.round((globalThis.performance?.now()??Date.now())*1e3))*1000n;return l().setBigUint64(s,i,!0),0},fd_close(r){return c.has(r)?(r>3&&c.delete(r),0):8},fd_fdstat_get(r,o){let s=c.get(r);if(!s)return 8;let i=l(),d=s.kind==="dir"?3:s.kind==="file"?4:2;return i.setUint8(o,d),i.setUint16(o+2,0,!0),i.setBigUint64(o+8,0xffffffffffffffffn,!0),i.setBigUint64(o+16,0xffffffffffffffffn,!0),0},fd_filestat_get(r,o){let s=c.get(r);return s?(s.kind==="dir"?k(o,3,0):s.kind==="file"?k(o,4,s.file.size):k(o,2,0),0):8},fd_filestat_set_size(r,o){let s=c.get(r);if(!s||s.kind!=="file")return 8;let i=Number(o);return P(s.file,i),i>s.file.size&&s.file.data.fill(0,s.file.size,i),s.file.size=i,0},fd_prestat_get(r,o){if(r!==3)return 8;let s=l();return s.setUint8(o,0),s.setUint32(o+4,1,!0),0},fd_prestat_dir_name(r,o,s){return r!==3?8:(s>=1&&(E()[o]=47),0)},fd_read(r,o,s,i){let d=c.get(r);if(!d)return 8;if(d.kind==="stdin"){let w=D({data:m,size:m.length},o,s,y);return y+=w,l().setUint32(i,w,!0),0}if(d.kind!=="file")return 8;let p=D(d.file,o,s,d.offset);return d.offset+=p,l().setUint32(i,p,!0),0},fd_pread(r,o,s,i,d){let p=c.get(r);if(!p||p.kind!=="file")return 8;let w=D(p.file,o,s,Number(i));return l().setUint32(d,w,!0),0},fd_write(r,o,s,i){let d=c.get(r);if(!d)return 8;if(d.kind==="stdout"||d.kind==="stderr"){let w=l(),A=0;for(let T=0;T<s;T++){let U=w.getUint32(o+T*8,!0),b=w.getUint32(o+T*8+4,!0);F(r,_(U,b)),A+=b}return w.setUint32(i,A,!0),0}if(d.kind!=="file")return 8;let p=Z(d.file,o,s,d.offset);return d.offset+=p,l().setUint32(i,p,!0),0},fd_pwrite(r,o,s,i,d){let p=c.get(r);if(!p||p.kind!=="file")return 8;let w=Z(p.file,o,s,Number(i));return l().setUint32(d,w,!0),0},fd_seek(r,o,s,i){let d=c.get(r);if(!d||d.kind!=="file")return 8;let p=Number(o),w=0;if(s===1)w=d.offset;else if(s===2)w=d.file.size;else if(s!==0)return 28;return w+p<0?28:(d.offset=w+p,l().setBigUint64(i,BigInt(d.offset),!0),0)},fd_readdir(r,o,s,i,d){let p=c.get(r);if(!p||p.kind!=="dir")return 8;p.entries||(p.entries=t.listDir(p.path));let w=l(),A=E(),T=0;for(let U=Number(i);U<p.entries.length;U++){let b=p.entries[U],x=M.encode(b.name),I=new Uint8Array(24+x.length),N=new DataView(I.buffer);N.setBigUint64(0,BigInt(U+1),!0),N.setBigUint64(8,BigInt(U+1),!0),N.setUint32(16,x.length,!0),N.setUint8(20,b.isDir?3:4),I.set(x,24);let j=Math.min(I.length,s-T);if(A.set(I.subarray(0,j),o+T),T+=j,j<I.length)break}return w.setUint32(d,T,!0),0},path_create_directory(r,o,s){let i=$(r,o,s);return i===null?8:t.exists(i)?20:t.dirs.has(H(i))?(t.dirs.add(i),0):44},path_filestat_get(r,o,s,i,d){let p=$(r,s,i);return p===null?8:C(p,d)},path_open(r,o,s,i,d,p,w,A,T){let U=$(r,s,i);if(U===null)return 8;let b=l();if(t.dirs.has(U)){if(d&4&&d&1)return 20;let N=g++;return c.set(N,{kind:"dir",path:U}),b.setUint32(T,N,!0),0}if(d&2)return t.files.has(U)?54:44;let x=t.files.get(U);if(x){if(d&4&&d&1)return 20;d&8&&(x.data=new Uint8Array(0),x.size=0)}else{if(!(d&1)||!t.dirs.has(H(U)))return 44;x={data:new Uint8Array(0),size:0},t.files.set(U,x)}let I=g++;return c.set(I,{kind:"file",path:U,file:x,offset:A&1?x.size:0}),b.setUint32(T,I,!0),0},path_remove_directory(r,o,s){let i=$(r,o,s);return i===null?8:t.dirs.has(i)?t.listDir(i).length>0?55:(t.dirs.delete(i),0):t.files.has(i)?54:44},path_unlink_file(r,o,s){let i=$(r,o,s);return i===null?8:t.files.has(i)?(t.files.delete(i),0):t.dirs.has(i)?31:44},fd_sync(){return 0},fd_datasync(){return 0},fd_advise(){return 0},fd_tell(r,o){let s=c.get(r);return!s||s.kind!=="file"?8:(l().setBigUint64(o,BigInt(s.offset),!0),0)},fd_renumber(r,o){return!c.has(r)||!c.has(o)?8:(c.set(o,c.get(r)),c.delete(r),0)},fd_allocate:()=>52,fd_fdstat_set_flags:()=>52,fd_fdstat_set_rights:()=>52,fd_filestat_set_times:()=>52,path_filestat_set_times:()=>52,path_link:()=>52,path_readlink:()=>52,path_rename:()=>52,path_symlink:()=>52,proc_raise:()=>52,sock_accept:()=>52,sock_recv:()=>52,sock_send:()=>52,sock_shutdown:()=>52,proc_exit(r){throw new B(r)},random_get(r,o){return globalThis.crypto.getRandomValues(E().subarray(r,r+o)),0},sched_yield(){return 0},poll_oneoff(){return 52}},setMemory(r){u=r},flush:O}},q=(t,e)=>{let n=new WebAssembly.Instance(t,{wasi_snapshot_preview1:e.imports});e.setMemory(n.exports.memory);let a=0;try{n.exports._start()}catch(f){if(f instanceof B)a=f.code;else throw e.flush(),f}return e.flush(),a};var L=class extends Error{constructor(e){super(`The program exited (code ${e}).`),this.name="JsExit",this.code=e}},Ot=new TextEncoder,It=new TextDecoder,st=t=>new Proxy(t,{get:(e,n)=>typeof n=="string"&&!(n in e)?()=>0:e[n]});function it({input:t="",args:e=[],stdout:n=()=>{},stderr:a=()=>{}}={}){let f=typeof t=="string"?Ot.encode(t):new Uint8Array(t),h=0,m=null,y=()=>new Uint8Array(m.buffer),c=(u,E)=>It.decode(y().subarray(u,u+E)),g=new Proxy({},{get:(u,E)=>typeof E=="string"?()=>0:void 0}),S={write(u,E,l){let _=c(E,l);(u===2?a:n)(_)},read(u,E,l){if(u!==0)return 0;let _=Math.max(0,Math.min(l,f.length-h));return _>0&&(y().set(f.subarray(h,h+_),E),h+=_),_},trap(){throw new L(1)},abort(){throw new L(1)},alert(){},evaluate(u,E){let l=c(u,E);(0,globalThis.eval)(l)},open(){},time_now:()=>BigInt(Date.now()),tick_now:()=>globalThis.performance?.now?.()??Date.now(),time_sleep(){},sqrt:Math.sqrt,sin:Math.sin,cos:Math.cos,pow:Math.pow,fmuladd:(u,E,l)=>u*E+l,ln:Math.log,exp:Math.exp,ldexp:(u,E)=>u*2**E,rand_bytes(u,E){y().set(globalThis.crypto.getRandomValues(new Uint8Array(E)),u)}};return{imports:{env:st({}),odin_env:st(S),odin_dom:g,webgl:g,webgl2:g},setMemory(u){m=u},args:e}}var J="/odin",ot="/out/main.wasm",z=["wasi_wasm32","js_wasm32"],Nt=new TextEncoder,at=()=>{let t=[],e=[],n=[];return{stdout:a=>{t.push(a),n.push(a)},stderr:a=>{e.push(a),n.push(a)},stdoutText:()=>t.join(""),stderrText:()=>e.join(""),outputText:()=>n.join("")}},Rt=t=>t.split(`
3
+ `).map(e=>e.trimEnd()).filter(e=>e.trim()!==""),ct=t=>t.crash?[t.crash]:t.diagnostics.length>0?t.diagnostics:[`The compiler produced no module (exit code ${t.exitCode}).`];function lt({compilerModule:t,libraryEntries:e}){let n=()=>{let c=new v;for(let[g,S]of e)c.writeFile(`${J}/${g}`,S);return c.mkdirAll(`${J}/vendor`),c},a=({code:c,target:g,compileArgs:S,fileName:u})=>{let E=n();E.writeFile(`/src/${u}`,Nt.encode(c)),E.mkdirAll("/out");let l=[],_=[],F=["odin","build","/src",`-out:${ot}`,"-backend:wasm",`-target:${g}`,...S],O=1,k="";try{O=q(t,X(E,{args:F,env:[`ODIN_ROOT=${J}`],stdout:C=>l.push(C),stderr:C=>_.push(C)}))}catch(C){k=C?.message??String(C)}return{exitCode:O,artifact:O===0?E.readFile(ot):null,diagnostics:Rt(l.join("")+_.join("")),crash:k}},f=({code:c,target:g,compileArgs:S=[],fileName:u="main.odin"})=>{let E=Y(),l=a({code:c,target:g,compileArgs:[...S],fileName:u}),_=Math.round(Y()-E);return l.artifact===null||l.artifact.length===0?{ok:!1,wasm:null,errors:ct(l),compileMs:_}:{ok:!0,wasm:l.artifact,errors:[],compileMs:_}},h=(c,{input:g,args:S})=>{let u=at(),E=0;try{E=q(new WebAssembly.Module(c.slice()),X(new v,{args:["main.wasm",...S],stdin:g,stdout:u.stdout,stderr:u.stderr}))}catch(l){l instanceof B?E=l.code:(u.stderr(`The program trapped: ${l?.message??l}
4
+ `),E=1)}return{exitCode:E,...dt(u)}},m=async(c,{input:g,args:S})=>{let u=at(),E=it({input:g,args:["main.wasm",...S],stdout:u.stdout,stderr:u.stderr}),l=0;try{let{instance:_}=await WebAssembly.instantiate(c.slice(),E.imports);E.setMemory(_.exports.memory),_.exports._start&&_.exports._start(),_.exports._end&&_.exports._end()}catch(_){_ instanceof L?l=_.code:(u.stderr(`The program trapped: ${_?.message??_}
5
+ `),l=1)}return{exitCode:l,...dt(u)}};return{compileAndRun:async({code:c,input:g="",target:S,args:u=[],compileArgs:E=[],fileName:l="main.odin"})=>{let _=Y(),F=a({code:c,target:S,compileArgs:[...E],fileName:l}),O=Math.round(Y()-_);if(F.artifact===null||F.artifact.length===0)return{stdout:"",stderr:"",output:"",errors:ct(F),exitCode:null,compileMs:O,runMs:null};let k=Y(),C;try{C=S==="js_wasm32"?await m(F.artifact,{input:g,args:u}):h(F.artifact,{input:g,args:u})}catch(D){C={exitCode:1,stdout:"",stderr:`The program could not run: ${D?.message??D}
6
+ `,output:""},C.output=C.stderr}let P=Math.round(Y()-k);return{stdout:C.stdout,stderr:C.stderr,output:C.output,errors:[],exitCode:C.exitCode,compileMs:O,runMs:P}},compileOnly:f}}var dt=t=>({stdout:t.stdoutText(),stderr:t.stderrText(),output:t.outputText()}),Y=()=>globalThis.performance?.now?.()??Date.now();var ut="ODINPOC1";function ft(t){let e=t instanceof Uint8Array?t:new Uint8Array(t);if(new TextDecoder().decode(e.subarray(0,8))!==ut)throw new Error(`The standard library pack is not a ${ut} container.`);let n=new DataView(e.buffer,e.byteOffset,e.byteLength),a=new TextDecoder,f=n.getUint32(8,!0),h=[],m=12;for(let y=0;y<f;y++){let c=n.getUint32(m,!0);m+=4;let g=a.decode(e.subarray(m,m+c));m+=c;let S=n.getUint32(m,!0);m+=4,h.push([g,e.subarray(m,m+S)]),m+=S}return h}var G=new Map;async function Dt(t,e){let n=y=>(c,g)=>e?.({name:y,loaded:c,total:g}),a=await t.readAsset("odin.wasm",n("odin.wasm")),f=await t.readAsset("odin-src.pack.gz",n("odin-src.pack.gz")),h=await WebAssembly.compile(a),m=ft(await nt(f));return lt({compilerModule:h,libraryEntries:m})}function Et(t,e){let n=G.get(t.key);return n===void 0&&(n={key:t.key,refs:0,promise:Dt(t,e)},G.set(t.key,n),n.promise.catch(()=>G.delete(t.key))),n.refs+=1,n}function K(t){t.refs-=1,t.refs<=0&&G.delete(t.key)}var Bt={wasi:"wasi_wasm32","wasi-wasm32":"wasi_wasm32",js:"js_wasm32","js-wasm32":"js_wasm32"};function Q(t,e){let n=t??e;if(n==null)return"wasi_wasm32";let a=Bt[n]??n;if(!z.includes(a))throw new Error(`Unknown target "${n}". Expected one of ${z.join(", ")}.`);return a}function ht({packaged:t}){async function e(n={}){let a=Q(n.target),f=et(n,t),h=Et(f,n.onProgress),m;try{m=await h.promise}catch(g){throw K(h),g}let y={target:a,args:n.args??[],compileArgs:n.compileArgs??[],fileName:n.fileName??"main.odin"},c=!1;return{get target(){return y.target},get targets(){return[...z]},async run(g,S,u={}){if(c)throw new Error("This compiler has been disposed.");if(typeof g!="string")throw new Error("run() needs the program source as its first argument.");return m.compileAndRun({code:g,input:S??"",target:Q(u.target,y.target),args:u.args??y.args,compileArgs:[...y.compileArgs,...u.compileArgs??[]],fileName:u.fileName??y.fileName})},async compile(g,S={}){if(c)throw new Error("This compiler has been disposed.");if(typeof g!="string")throw new Error("compile() needs the program source as its first argument.");return m.compileOnly({code:g,target:Q(S.target,y.target),compileArgs:[...y.compileArgs,...S.compileArgs??[]],fileName:S.fileName??y.fileName})},dispose(){c||(c=!0,K(h))}}}return{createCompiler:e,TARGETS:z}}var pt=ht({packaged:null}),Lt=pt.createCompiler,{TARGETS:Yt}=pt;return yt($t);})();
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@live-codes/odin-wasm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Run Odin in the browser with one API, on the Odin compiler compiled to WebAssembly",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "node": "./src/index.node.js",
13
+ "browser": "./src/index.js",
14
+ "default": "./src/index.js"
15
+ },
16
+ "./iife": "./dist/odin-wasm.global.js"
17
+ },
18
+ "bin": {
19
+ "odin-wasm-copy-assets": "./bin/copy-assets.mjs"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "bin",
24
+ "scripts",
25
+ "dist",
26
+ "assets",
27
+ "THIRD-PARTY-NOTICES.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "scripts": {
33
+ "sync-assets": "node scripts/sync-assets.mjs",
34
+ "build:iife": "node scripts/build-iife.mjs",
35
+ "check-package": "node scripts/check-package.mjs",
36
+ "prepack": "node scripts/check-package.mjs",
37
+ "check": "node --check src/api.js && node --check src/assets.js && node --check src/asset-receipts.js && node --check src/engine.js && node --check src/host-js.js && node --check src/pack.js && node --check src/runtime.js && node --check src/wasi-host.js && node --check src/index.js && node --check src/index.node.js && node --check src/packaged.node.js && node --check bin/copy-assets.mjs && node --check scripts/sync-assets.mjs && node --check scripts/build-iife.mjs && node --check scripts/check-package.mjs",
38
+ "test": "node --test \"test/*.test.js\""
39
+ },
40
+ "devDependencies": {
41
+ "esbuild": "^0.25.0"
42
+ },
43
+ "keywords": [
44
+ "odin",
45
+ "wasm",
46
+ "webassembly",
47
+ "compiler",
48
+ "playground",
49
+ "livecodes"
50
+ ]
51
+ }
@@ -0,0 +1,33 @@
1
+ // Builds the IIFE bundle: one classic script, for workers and pages that cannot
2
+ // use ES modules.
3
+ //
4
+ // npm run build:iife
5
+ //
6
+ // The output is committed, because a classic worker can only `importScripts()`
7
+ // a URL and a consumer should not need a bundler to get one. It is built from
8
+ // the browser entry, so it never pulls in the Node-only packaged-assets code.
9
+ import { build } from 'esbuild';
10
+ import { statSync } from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const BUNDLE = {
14
+ entry: '../src/index.js',
15
+ outfile: '../dist/odin-wasm.global.js',
16
+ globalName: 'odinWasm',
17
+ banner: `/*! @live-codes/odin-wasm - MIT. IIFE build, sets self.odinWasm.
18
+ * importScripts('odin-wasm.global.js') then self.odinWasm.createCompiler({ baseUrl }).
19
+ * The compiler assets it fetches are Odin's, under the zlib license. */`
20
+ };
21
+
22
+ const out = fileURLToPath(new URL(BUNDLE.outfile, import.meta.url));
23
+ await build({
24
+ entryPoints: [fileURLToPath(new URL(BUNDLE.entry, import.meta.url))],
25
+ outfile: out,
26
+ bundle: true,
27
+ format: 'iife',
28
+ globalName: BUNDLE.globalName,
29
+ minify: true,
30
+ platform: 'browser',
31
+ target: 'es2022'
32
+ });
33
+ console.log(`${BUNDLE.outfile.replace('../', '')} ${(statSync(out).size / 1024).toFixed(1)} KB (minified)`);
@@ -0,0 +1,43 @@
1
+ // Refuses to pack a package that cannot work.
2
+ //
3
+ // npm run check-package (also the `prepack` hook, so `npm publish` runs it)
4
+ //
5
+ // The assets are committed, so they can go stale or be deleted without anything in the tests
6
+ // noticing — the tests read whatever is there. This checks that every asset the receipts name is
7
+ // present and hashes to its receipt, and that the built bundle exists, before anything is published.
8
+ import { createHash } from 'node:crypto';
9
+ import { existsSync } from 'node:fs';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { ASSET_RECEIPTS } from '../src/asset-receipts.js';
12
+
13
+ const ASSETS = new URL('../assets/', import.meta.url);
14
+ const BUNDLE = new URL('../dist/odin-wasm.global.js', import.meta.url);
15
+
16
+ const problems = [];
17
+
18
+ for (const [name, receipt] of Object.entries(ASSET_RECEIPTS)) {
19
+ const url = new URL(name, ASSETS);
20
+ if (!existsSync(url)) {
21
+ problems.push(`missing asset: assets/${name}`);
22
+ continue;
23
+ }
24
+ const bytes = await readFile(url);
25
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
26
+ if (bytes.length !== receipt.bytes || sha256 !== receipt.sha256) {
27
+ problems.push(`assets/${name} does not match its receipt (run \`npm run sync-assets\`)`);
28
+ }
29
+ }
30
+
31
+ if (!existsSync(BUNDLE)) {
32
+ problems.push('missing dist/odin-wasm.global.js (run `npm run build:iife`)');
33
+ }
34
+
35
+ if (problems.length > 0) {
36
+ console.error('Refusing to pack:');
37
+ for (const problem of problems) console.error(` ${problem}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(
42
+ `Ready to pack: ${Object.keys(ASSET_RECEIPTS).length} assets verified, the bundle is present.`
43
+ );
@@ -0,0 +1,63 @@
1
+ // Copies the assets the repository builds into this package, and rewrites
2
+ // `src/asset-receipts.js` to pin them.
3
+ //
4
+ // npm run sync-assets (from this package)
5
+ // npm run assets:package (from the repository root, which builds first)
6
+ //
7
+ // The repository's `scripts/build-assets.sh` builds `assets/odin.wasm` and
8
+ // `assets/odin-src.pack.gz` at the root; a package has to carry its own copy, so
9
+ // this is the one step between them.
10
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
11
+ import { createHash } from 'node:crypto';
12
+
13
+ const ROOT_ASSETS = new URL('../../../assets/', import.meta.url);
14
+ const PUBLIC = new URL('../../../public/', import.meta.url);
15
+ const PACKAGE_ASSETS = new URL('../assets/', import.meta.url);
16
+ const RECEIPTS = new URL('../src/asset-receipts.js', import.meta.url);
17
+
18
+ // What ships in assets/, in the order the package loads it, and where each comes from.
19
+ const ASSETS = [
20
+ ['odin.wasm', new URL('odin.wasm', ROOT_ASSETS)],
21
+ ['odin-src.pack.gz', new URL('odin-src.pack.gz', ROOT_ASSETS)],
22
+ // Odin's own JS runtime, which is what a js_wasm32 program runs on when it needs the DOM — a
23
+ // worker has no `document`, so the host instantiates on the page. The repository vendors it for
24
+ // its own page; this is the same file.
25
+ ['odin.js', new URL('odin.js', PUBLIC)],
26
+ ];
27
+
28
+ await mkdir(PACKAGE_ASSETS, { recursive: true });
29
+
30
+ const receipts = [];
31
+ for (const [name, from] of ASSETS) {
32
+ await copyFile(from, new URL(name, PACKAGE_ASSETS));
33
+ const bytes = await readFile(new URL(name, PACKAGE_ASSETS));
34
+ receipts.push({
35
+ name,
36
+ bytes: bytes.length,
37
+ sha256: createHash('sha256').update(bytes).digest('hex'),
38
+ });
39
+ }
40
+
41
+ const entries = receipts
42
+ .map(({ name, bytes, sha256 }) => `\t'${name}': Object.freeze({ bytes: ${bytes}, sha256: '${sha256}' })`)
43
+ .join(',\n');
44
+
45
+ const body = `// Receipts for the assets this package ships in \`assets/\`, keyed by their path under it.
46
+ //
47
+ // Generated by scripts/sync-assets.mjs — do not edit by hand.
48
+ //
49
+ // Every read is checked against one of these before the bytes are used, so a truncated download or
50
+ // a host serving something else is an error here rather than a wasm instantiation failure a step
51
+ // later.
52
+
53
+ export const ASSET_RECEIPTS = Object.freeze({
54
+ ${entries}
55
+ });
56
+ `;
57
+
58
+ await writeFile(RECEIPTS, body);
59
+
60
+ for (const { name, bytes } of receipts) {
61
+ console.log(` ${name} ${bytes} bytes`);
62
+ }
63
+ console.log(`Wrote assets/ and src/asset-receipts.js`);
package/src/api.js ADDED
@@ -0,0 +1,140 @@
1
+ import { resolveAssetSource } from './assets.js';
2
+ import { TARGETS } from './engine.js';
3
+ import { acquireEngine, releaseEngine } from './runtime.js';
4
+
5
+ // One implementation, two entry points: `index.js` for anything without a
6
+ // filesystem, and `index.node.js` for Node, which can also read the assets that
7
+ // ship in this package.
8
+
9
+ const TARGET_ALIASES = {
10
+ wasi: 'wasi_wasm32',
11
+ 'wasi-wasm32': 'wasi_wasm32',
12
+ js: 'js_wasm32',
13
+ 'js-wasm32': 'js_wasm32',
14
+ };
15
+
16
+ function resolveTarget(value, fallback) {
17
+ const target = value ?? fallback;
18
+ if (target == null) return 'wasi_wasm32';
19
+ const resolved = TARGET_ALIASES[target] ?? target;
20
+ if (!TARGETS.includes(resolved)) {
21
+ throw new Error(`Unknown target "${target}". Expected one of ${TARGETS.join(', ')}.`);
22
+ }
23
+ return resolved;
24
+ }
25
+
26
+ export function createApi({ packaged }) {
27
+ /**
28
+ * Create an Odin compiler.
29
+ *
30
+ * The heavy part — the compiler module and the packed standard library — is
31
+ * fetched here, so a bad `baseUrl` fails at this point rather than at the
32
+ * first run. Sources that resolve to the same assets share one engine, so two
33
+ * compilers against the same `baseUrl` load the bytes once.
34
+ *
35
+ * @param {object} [options]
36
+ * @param {string} [options.baseUrl] - where the assets are served from. In
37
+ * Node this may be omitted to use the files that ship in this package;
38
+ * anywhere else it is required.
39
+ * @param {'wasi_wasm32'|'js_wasm32'} [options.target] - what the program is
40
+ * compiled for. `wasi_wasm32` (the default) runs anywhere; `js_wasm32` is
41
+ * the browser target, and outside a page only its console output works.
42
+ * @param {string[]} [options.args] - default program argv.
43
+ * @param {string[]} [options.compileArgs] - extra compiler flags.
44
+ * @param {string} [options.fileName] - the name the source is compiled under.
45
+ * @param {(progress: {name: string, loaded: number, total: number}) => void} [options.onProgress]
46
+ */
47
+ async function createCompiler(options = {}) {
48
+ const target = resolveTarget(options.target);
49
+ const source = resolveAssetSource(options, packaged);
50
+ const entry = acquireEngine(source, options.onProgress);
51
+
52
+ let engine;
53
+ try {
54
+ engine = await entry.promise;
55
+ } catch (error) {
56
+ releaseEngine(entry);
57
+ throw error;
58
+ }
59
+
60
+ const defaults = {
61
+ target,
62
+ args: options.args ?? [],
63
+ compileArgs: options.compileArgs ?? [],
64
+ fileName: options.fileName ?? 'main.odin',
65
+ };
66
+ let disposed = false;
67
+
68
+ return {
69
+ /** The target this compiler builds for, by default. */
70
+ get target() {
71
+ return defaults.target;
72
+ },
73
+
74
+ /** Every target the package can run, for building a picker. */
75
+ get targets() {
76
+ return [...TARGETS];
77
+ },
78
+
79
+ /**
80
+ * Compile and run a program.
81
+ *
82
+ * @param {string} code - the Odin source.
83
+ * @param {string|Uint8Array} [input] - stdin, handed to the program once.
84
+ * @param {object} [runOptions] - per-run `target`, `args`, `compileArgs`,
85
+ * `fileName`.
86
+ * @returns {Promise<{stdout: string, stderr: string, output: string,
87
+ * errors: string[], exitCode: number|null, compileMs: number,
88
+ * runMs: number|null}>} `output` is stdout and stderr in the order the
89
+ * program wrote them; `errors` holds the compiler's diagnostics and is
90
+ * empty when it compiled; `exitCode` is null when the program never ran.
91
+ */
92
+ async run(code, input, runOptions = {}) {
93
+ if (disposed) throw new Error('This compiler has been disposed.');
94
+ if (typeof code !== 'string') {
95
+ throw new Error('run() needs the program source as its first argument.');
96
+ }
97
+ return engine.compileAndRun({
98
+ code,
99
+ input: input ?? '',
100
+ target: resolveTarget(runOptions.target, defaults.target),
101
+ args: runOptions.args ?? defaults.args,
102
+ compileArgs: [...defaults.compileArgs, ...(runOptions.compileArgs ?? [])],
103
+ fileName: runOptions.fileName ?? defaults.fileName,
104
+ });
105
+ },
106
+
107
+ /**
108
+ * Compile without running, for a host that runs the module itself.
109
+ *
110
+ * A page that needs the DOM cannot use `run`: a worker has no `document`, so the module has to
111
+ * be instantiated on the page with Odin's own JS runtime. This hands back the module for that.
112
+ *
113
+ * @param {string} code - the Odin source.
114
+ * @param {object} [options] - per-call `target`, `compileArgs`, `fileName`.
115
+ * @returns {Promise<{ok: boolean, wasm: Uint8Array|null, errors: string[], compileMs: number}>}
116
+ */
117
+ async compile(code, options = {}) {
118
+ if (disposed) throw new Error('This compiler has been disposed.');
119
+ if (typeof code !== 'string') {
120
+ throw new Error('compile() needs the program source as its first argument.');
121
+ }
122
+ return engine.compileOnly({
123
+ code,
124
+ target: resolveTarget(options.target, defaults.target),
125
+ compileArgs: [...defaults.compileArgs, ...(options.compileArgs ?? [])],
126
+ fileName: options.fileName ?? defaults.fileName,
127
+ });
128
+ },
129
+
130
+ /** Drop this compiler's hold on the shared engine. Further runs throw. */
131
+ dispose() {
132
+ if (disposed) return;
133
+ disposed = true;
134
+ releaseEngine(entry);
135
+ },
136
+ };
137
+ }
138
+
139
+ return { createCompiler, TARGETS };
140
+ }
@@ -0,0 +1,13 @@
1
+ // Receipts for the assets this package ships in `assets/`, keyed by their path under it.
2
+ //
3
+ // Generated by scripts/sync-assets.mjs — do not edit by hand.
4
+ //
5
+ // Every read is checked against one of these before the bytes are used, so a truncated download or
6
+ // a host serving something else is an error here rather than a wasm instantiation failure a step
7
+ // later.
8
+
9
+ export const ASSET_RECEIPTS = Object.freeze({
10
+ 'odin.wasm': Object.freeze({ bytes: 3763439, sha256: '5044e383aa2415927cd518a4d3e6fdbcc6c99154ac90d212e154f18c092deef3' }),
11
+ 'odin-src.pack.gz': Object.freeze({ bytes: 2502169, sha256: '567b1c0c1b50d3e557d119927ade3373dda91364acae3302f2cf01950a8dd8f9' }),
12
+ 'odin.js': Object.freeze({ bytes: 72772, sha256: 'e918ba1614af23f02299976122d5edf4c994873ce4c6cdab621d42f3e9cb9733' })
13
+ });
package/src/assets.js ADDED
@@ -0,0 +1,149 @@
1
+ // Where the assets come from, and how they are read.
2
+ //
3
+ // Two sources behind one shape, because everything downstream only needs "give
4
+ // me this asset, verified":
5
+ //
6
+ // hosted - a base URL. The page fetches from a server you control.
7
+ // packaged - the files that ship inside this package, on disk. Only reachable
8
+ // in Node, which is the only place there is a filesystem.
9
+ //
10
+ // Every asset is checked against a pinned SHA-256 receipt before it is used, so
11
+ // a truncated download or a host serving something else is an error here rather
12
+ // than a wasm instantiation failure a step later.
13
+
14
+ import { ASSET_RECEIPTS } from './asset-receipts.js';
15
+
16
+ /** The assets the package carries, in the order they are loaded. */
17
+ export const ASSET_NAMES = Object.freeze(Object.keys(ASSET_RECEIPTS));
18
+
19
+ const BASE_URL_REQUIRED =
20
+ 'baseUrl is required here. The assets that ship in this package can only be read where there is a ' +
21
+ 'filesystem, and a browser cannot reach a file inside an npm package — copy them somewhere your ' +
22
+ 'page can fetch with `npx --package @live-codes/odin-wasm odin-wasm-copy-assets <dir>` and pass ' +
23
+ 'that directory as baseUrl.';
24
+
25
+ /**
26
+ * @param {object} options - the options `createCompiler` was given
27
+ * @param {object|null} packaged - the packaged-asset reader, or null where there is no filesystem
28
+ */
29
+ export function resolveAssetSource(options, packaged) {
30
+ if (options.baseUrl != null && options.baseUrl !== '') return createHostedSource(options);
31
+ if (!packaged) throw new Error(BASE_URL_REQUIRED);
32
+ return createPackagedSource(packaged);
33
+ }
34
+
35
+ function createHostedSource(options) {
36
+ const baseUrl = resolveBaseUrl(options.baseUrl);
37
+ return {
38
+ kind: 'hosted',
39
+ key: `hosted\u0000${baseUrl}`,
40
+ description: baseUrl,
41
+ baseUrl,
42
+ async readAsset(name, onProgress) {
43
+ const url = new URL(name, baseUrl);
44
+ const response = await fetch(url);
45
+ if (!response.ok) {
46
+ throw new Error(`Failed to load the runtime asset ${url}: ${response.status}`);
47
+ }
48
+ return verifyReceipt(name, await readBody(response, onProgress));
49
+ },
50
+ };
51
+ }
52
+
53
+ function createPackagedSource(packaged) {
54
+ return {
55
+ kind: 'packaged',
56
+ key: `packaged\u0000${packaged.root.href}`,
57
+ description: `the assets packaged with this library (${packaged.root.href})`,
58
+ async readAsset(name) {
59
+ let bytes;
60
+ try {
61
+ bytes = await packaged.readFile(name);
62
+ } catch (error) {
63
+ throw new Error(
64
+ `Failed to read the packaged asset ${name} from ${packaged.root.href}: ${error.message}`,
65
+ { cause: error }
66
+ );
67
+ }
68
+ return verifyReceipt(name, bytes);
69
+ },
70
+ };
71
+ }
72
+
73
+ // Absolute http(s); in a browser a relative URL resolves against the page.
74
+ function resolveBaseUrl(value) {
75
+ let url;
76
+ try {
77
+ url = value instanceof URL ? value : new URL(value, globalThis.document?.baseURI);
78
+ } catch (error) {
79
+ throw new Error(
80
+ `baseUrl must be an absolute http(s) URL, or relative to the page in a browser: ${error.message}`,
81
+ { cause: error }
82
+ );
83
+ }
84
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
85
+ throw new Error(
86
+ `baseUrl must be an absolute http(s) URL, or relative to the page in a browser: got "${value}"`
87
+ );
88
+ }
89
+ return url.href.endsWith('/') ? url.href : `${url.href}/`;
90
+ }
91
+
92
+ async function readBody(response, onProgress) {
93
+ if (!onProgress || !response.body) {
94
+ return new Uint8Array(await response.arrayBuffer());
95
+ }
96
+ const total = Number(response.headers.get('content-length')) || 0;
97
+ const reader = response.body.getReader();
98
+ const chunks = [];
99
+ let loaded = 0;
100
+ onProgress(0, total);
101
+ for (;;) {
102
+ const { done, value } = await reader.read();
103
+ if (done) break;
104
+ chunks.push(value);
105
+ loaded += value.length;
106
+ onProgress(loaded, total);
107
+ }
108
+ const bytes = new Uint8Array(loaded);
109
+ let offset = 0;
110
+ for (const chunk of chunks) {
111
+ bytes.set(chunk, offset);
112
+ offset += chunk.length;
113
+ }
114
+ return bytes;
115
+ }
116
+
117
+ async function verifyReceipt(name, bytes) {
118
+ const receipt = ASSET_RECEIPTS[name];
119
+ if (!receipt) throw new Error(`No pinned receipt for the runtime asset ${name}`);
120
+ if (bytes.byteLength !== receipt.bytes) {
121
+ throw new Error(`The runtime asset ${name} is ${bytes.byteLength} bytes, expected ${receipt.bytes}`);
122
+ }
123
+ const digest = await sha256Hex(bytes);
124
+ if (digest !== receipt.sha256) {
125
+ throw new Error(
126
+ `The runtime asset ${name} failed SHA-256 verification: expected ${receipt.sha256}, got ${digest}`
127
+ );
128
+ }
129
+ return bytes;
130
+ }
131
+
132
+ export async function sha256Hex(bytes) {
133
+ const subtle = globalThis.crypto?.subtle;
134
+ if (!subtle) {
135
+ throw new Error(
136
+ 'Verifying the runtime assets needs crypto.subtle: a secure context in the browser, or Node 20 and later.'
137
+ );
138
+ }
139
+ const digest = await subtle.digest('SHA-256', bytes);
140
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
141
+ }
142
+
143
+ export async function inflateGzip(bytes) {
144
+ if (typeof DecompressionStream !== 'function') {
145
+ throw new Error('Inflating the standard library pack needs DecompressionStream');
146
+ }
147
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
148
+ return new Uint8Array(await new Response(stream).arrayBuffer());
149
+ }