@gjsify/fetch 0.25.1 → 0.26.1

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.
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{Blob as e,File as t}from"./utils/blob-from.js";import{FetchError as n}from"./errors/fetch-error.js";import{isDomainOrSubdomain as r,isSameProtocol as i}from"./utils/is.js";import{clone as a}from"./body.js";import o from"./headers.js";import{parseDataUri as s}from"./utils/data-uri.js";import{isRedirect as c}from"./utils/is-redirect.js";import{Response as l}from"./response.js";import{parseReferrerPolicyFromHeader as u}from"./utils/referrer.js";import{Request as d,getSoupRequestOptions as f}from"./request.js";import{AbortError as p}from"./errors/abort-error.js";import{XMLHttpRequest as m,XMLHttpRequestUpload as h}from"./xhr.js";import{URL as g}from"@gjsify/url";import _ from"node:stream";import{FormData as v}from"@gjsify/formdata";import y from"@girs/glib-2.0";import b from"system";const x=new Set([`data:`,`http:`,`https:`,`file:`]);function rewriteRootRelativeUrl(e){if(typeof e!=`string`||!e.startsWith(`/`)||e.startsWith(`//`))return e;let t=globalThis.__GJSIFY_DEBUG_FETCH===!0;try{let n=b.programPath??b.programInvocationName??``;if(!n)return e;let r=`file://${y.path_get_dirname(n)}${e}`;return t&&console.log(`[fetch] rewrite ${e} → ${r}`),r}catch(n){return t&&console.warn(`[fetch] rewrite FAILED: ${n instanceof Error?n.message:String(n)}`),e}}async function fetch(t,m={}){t=rewriteRootRelativeUrl(t);let h=new d(t,m),{parsedURL:v,options:b}=f(h);if(!x.has(v.protocol))throw TypeError(`@gjsify/fetch cannot load ${t}. URL scheme "${v.protocol.replace(/:$/,``)}" is not supported.`);if(v.protocol===`data:`){let{buffer:e,typeFull:t}=s(h.url);return new l(Buffer.from(e),{headers:{"Content-Type":t}})}if(v.protocol===`file:`){let e=globalThis.__GJSIFY_DEBUG_FETCH===!0;e&&console.log(`[fetch] file:// ${h.url}`);try{let t=y.filename_from_uri(h.url)[0];e&&console.log(`[fetch] file:// path=${t}`);let[r,i]=y.file_get_contents(t);if(e&&console.log(`[fetch] file:// ok=${r} bytes=${i?.byteLength??`?`}`),!r)throw new n(`Failed to read file: ${t}`,`system`);let a=i,o=new Uint8Array(a.byteLength);o.set(a);let s=new l(o);return e&&console.log(`[fetch] file:// response created`),s}catch(t){let r=t instanceof Error?t:Error(String(t));throw e&&console.warn(`[fetch] file:// FAIL: ${r.message}`),new n(`request to ${h.url} failed, reason: ${r.message}`,`system`,r)}}let{signal:S}=h;if(S&&S.aborted)throw new p(`The operation was aborted.`);let C,w;try{let e=await h._send(b);C=e.readable,w=e.cancellable}catch(e){if(S&&S.aborted)throw new p(`The operation was aborted.`);let t=e instanceof Error?e:Error(String(e));throw new n(`request to ${h.url} failed, reason: ${t.message}`,`system`,t)}let abortHandler=()=>{w.cancel()};S&&S.addEventListener(`abort`,abortHandler,{once:!0});let finalize=()=>{S&&S.removeEventListener(`abort`,abortHandler)};w.connect(()=>{C.destroy(new p(`The operation was aborted.`))}),C.on(`error`,e=>{finalize()});let T=h._message,E=o._newFromSoupMessage(T),D=T.status_code,O=T.get_reason_phrase();if(c(D)){let e=E.get(`Location`),t=null;try{t=e===null?null:new g(e,h.url)}catch{if(h.redirect!==`manual`)throw finalize(),new n(`uri requested responds with an invalid redirect URL: ${e}`,`invalid-redirect`)}switch(h.redirect){case`error`:throw finalize(),new n(`uri requested responds with a redirect, redirect mode is set to error: ${h.url}`,`no-redirect`);case`manual`:break;case`follow`:{if(t===null)break;if(h.counter>=h.follow)throw finalize(),new n(`maximum redirect reached at: ${h.url}`,`max-redirect`);let e={headers:new o(h.headers),follow:h.follow,counter:h.counter+1,agent:h.agent,compress:h.compress,method:h.method,body:a(h),signal:h.signal,size:h.size,referrer:h.referrer,referrerPolicy:h.referrerPolicy};if(!r(h.url,t)||!i(h.url,t))for(let t of[`authorization`,`www-authenticate`,`cookie`,`cookie2`])e.headers.delete(t);if(D!==303&&h.body&&m.body instanceof _.Readable)throw finalize(),new n(`Cannot follow redirect with body being a readable stream`,`unsupported-redirect`);(D===303||(D===301||D===302)&&h.method===`POST`)&&(e.method=`GET`,e.body=void 0,e.headers.delete(`content-length`));let s=u(E);return s&&(e.referrerPolicy=s),finalize(),fetch(new d(t,e))}default:throw TypeError(`Redirect option '${h.redirect}' is not a valid value of RequestRedirect`)}}let k={url:h.url,status:D,statusText:O,headers:E,size:h.size,counter:h.counter,highWaterMark:h.highWaterMark},A=E.get(`Content-Encoding`);if(!h.compress||h.method===`HEAD`||A===null||D===204||D===304)return finalize(),new l(C,k);if(typeof DecompressionStream<`u`){let t=null;if(A===`gzip`||A===`x-gzip`?t=`gzip`:(A===`deflate`||A===`x-deflate`)&&(t=`deflate`),t){let n=new e([await new l(C,k).arrayBuffer()]).stream().pipeThrough(new DecompressionStream(t));return finalize(),new l(n,k)}}return finalize(),new l(C,k)}export{p as AbortError,e as Blob,n as FetchError,t as File,v as FormData,o as Headers,d as Request,l as Response,m as XMLHttpRequest,h as XMLHttpRequestUpload,fetch as default,c as isRedirect};
1
+ import"./_virtual/_rolldown/runtime.js";import{Blob as e,File as t}from"./utils/blob-from.js";import{FetchError as n}from"./errors/fetch-error.js";import{isDomainOrSubdomain as r,isSameProtocol as i}from"./utils/is.js";import{clone as a}from"./body.js";import{AbortError as o}from"./errors/abort-error.js";import s from"./headers.js";import{parseDataUri as c}from"./utils/data-uri.js";import{resolveRootRelativeUrl as l}from"./utils/root-relative-system.js";import{isRedirect as u}from"./utils/is-redirect.js";import{Response as d}from"./response.js";import{parseReferrerPolicyFromHeader as f}from"./utils/referrer.js";import{Request as p,getSoupRequestOptions as m}from"./request.js";import{XMLHttpRequest as h,XMLHttpRequestUpload as g}from"./xhr.js";import{URL as _}from"@gjsify/url";import v from"node:stream";import{FormData as y}from"@gjsify/formdata";import b from"@girs/glib-2.0";const x=new Set([`data:`,`http:`,`https:`,`file:`]);function rewriteFetchInput(e){if(typeof e!=`string`)return e;let t=l(e);return t!==e&&globalThis.__GJSIFY_DEBUG_FETCH===!0&&console.log(`[fetch] rewrite ${e} → ${t}`),t}async function fetch(t,l={}){t=rewriteFetchInput(t);let h=new p(t,l),{parsedURL:g,options:y}=m(h);if(!x.has(g.protocol))throw TypeError(`@gjsify/fetch cannot load ${t}. URL scheme "${g.protocol.replace(/:$/,``)}" is not supported.`);if(g.protocol===`data:`){let{buffer:e,typeFull:t}=c(h.url);return new d(Buffer.from(e),{headers:{"Content-Type":t}})}if(g.protocol===`file:`){let e=globalThis.__GJSIFY_DEBUG_FETCH===!0;e&&console.log(`[fetch] file:// ${h.url}`);try{let t=b.filename_from_uri(h.url)[0];e&&console.log(`[fetch] file:// path=${t}`);let[r,i]=b.file_get_contents(t);if(e&&console.log(`[fetch] file:// ok=${r} bytes=${i?.byteLength??`?`}`),!r)throw new n(`Failed to read file: ${t}`,`system`);let a=i,o=new Uint8Array(a.byteLength);o.set(a);let s=new d(o);return e&&console.log(`[fetch] file:// response created`),s}catch(t){let r=t instanceof Error?t:Error(String(t));throw e&&console.warn(`[fetch] file:// FAIL: ${r.message}`),new n(`request to ${h.url} failed, reason: ${r.message}`,`system`,r)}}let{signal:S}=h;if(S&&S.aborted)throw new o(`The operation was aborted.`);let C,w;try{let e=await h._send(y);C=e.readable,w=e.cancellable}catch(e){if(S&&S.aborted)throw new o(`The operation was aborted.`);let t=e instanceof Error?e:Error(String(e));throw new n(`request to ${h.url} failed, reason: ${t.message}`,`system`,t)}let abortHandler=()=>{w.cancel()};S&&S.addEventListener(`abort`,abortHandler,{once:!0});let finalize=()=>{S&&S.removeEventListener(`abort`,abortHandler)};w.connect(()=>{C.destroy(new o(`The operation was aborted.`))}),C.on(`error`,e=>{finalize()});let T=h._message,E=s._newFromSoupMessage(T),D=T.status_code,O=T.get_reason_phrase();if(u(D)){let e=E.get(`Location`),t=null;try{t=e===null?null:new _(e,h.url)}catch{if(h.redirect!==`manual`)throw finalize(),new n(`uri requested responds with an invalid redirect URL: ${e}`,`invalid-redirect`)}switch(h.redirect){case`error`:throw finalize(),new n(`uri requested responds with a redirect, redirect mode is set to error: ${h.url}`,`no-redirect`);case`manual`:break;case`follow`:{if(t===null)break;if(h.counter>=h.follow)throw finalize(),new n(`maximum redirect reached at: ${h.url}`,`max-redirect`);let e={headers:new s(h.headers),follow:h.follow,counter:h.counter+1,agent:h.agent,compress:h.compress,method:h.method,body:a(h),signal:h.signal,size:h.size,referrer:h.referrer,referrerPolicy:h.referrerPolicy};if(!r(h.url,t)||!i(h.url,t))for(let t of[`authorization`,`www-authenticate`,`cookie`,`cookie2`])e.headers.delete(t);if(D!==303&&h.body&&l.body instanceof v.Readable)throw finalize(),new n(`Cannot follow redirect with body being a readable stream`,`unsupported-redirect`);(D===303||(D===301||D===302)&&h.method===`POST`)&&(e.method=`GET`,e.body=void 0,e.headers.delete(`content-length`));let o=f(E);return o&&(e.referrerPolicy=o),finalize(),fetch(new p(t,e))}default:throw TypeError(`Redirect option '${h.redirect}' is not a valid value of RequestRedirect`)}}let k={url:h.url,status:D,statusText:O,headers:E,size:h.size,counter:h.counter,highWaterMark:h.highWaterMark},A=E.get(`Content-Encoding`);if(!h.compress||h.method===`HEAD`||A===null||D===204||D===304)return finalize(),new d(C,k);if(typeof DecompressionStream<`u`){let t=null;if(A===`gzip`||A===`x-gzip`?t=`gzip`:(A===`deflate`||A===`x-deflate`)&&(t=`deflate`),t){let n=new e([await new d(C,k).arrayBuffer()]).stream().pipeThrough(new DecompressionStream(t));return finalize(),new d(n,k)}}return finalize(),new d(C,k)}export{o as AbortError,e as Blob,n as FetchError,t as File,y as FormData,s as Headers,p as Request,d as Response,h as XMLHttpRequest,g as XMLHttpRequestUpload,fetch as default,u as isRedirect,l as resolveRootRelativeUrl};
@@ -1 +1 @@
1
- import{XMLHttpRequest as e,XMLHttpRequestUpload as t}from"../xhr.js";import n from"gi://GLib?version=2.0";globalThis.XMLHttpRequest===void 0&&(globalThis.XMLHttpRequest=e),globalThis.XMLHttpRequestUpload===void 0&&(globalThis.XMLHttpRequestUpload=t);const r=globalThis.URL;if(r&&r.__gjsify_objecturl!==!0){let e=new Map,t=r.createObjectURL?.bind(r),i=r.revokeObjectURL?.bind(r);r.createObjectURL=n=>{let r=n?._tmpPath;if(typeof r==`string`&&r.length>0){let t=`file://${r}`;return e.set(t,r),t}return t?t(n):`file:///dev/null`},r.revokeObjectURL=t=>{let r=e.get(t);if(r){try{n.unlink(r)}catch{}e.delete(t)}else i&&i(t)},r.__gjsify_objecturl=!0}
1
+ import{XMLHttpRequest as e,XMLHttpRequestUpload as t}from"../xhr.js";import n from"gi://GLib?version=2.0";globalThis.XMLHttpRequest===void 0&&(globalThis.XMLHttpRequest=e),globalThis.XMLHttpRequestUpload===void 0&&(globalThis.XMLHttpRequestUpload=t);const r=globalThis.URL;if(r&&r.__gjsify_objecturl!==!0){let e=new Map,t=r.createObjectURL?.bind(r),i=r.revokeObjectURL?.bind(r);r.createObjectURL=n=>{let r=n?._tmpPath;if(typeof r==`string`&&r.length>0){let t=`file://${r}`;return e.set(t,r),t}return t?t(n):`file:///dev/null`},r.revokeObjectURL=t=>{let r=e.get(t);r?(n.unlink(r),e.delete(t)):i&&i(t)},r.__gjsify_objecturl=!0}
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`);let g=null;function envPositiveInt(e,t){let n=p.getenv(e);if(!n)return t;let r=Number.parseInt(n,10);return Number.isFinite(r)&&r>0?r:t}function shouldAcceptAnyCert(e){return e===!1?!0:e===!0?!1:p.getenv(`NODE_TLS_REJECT_UNAUTHORIZED`)===`0`}function getSharedSession(){if(g===null){let e=envPositiveInt(`GJSIFY_FETCH_MAX_CONNS_PER_HOST`,16),t=Math.max(envPositiveInt(`GJSIFY_FETCH_MAX_CONNS`,64),e);g=new f.Session({maxConns:t,maxConnsPerHost:e});try{g.remove_feature_by_type(f.ContentDecoder.$gtype)}catch{}}return g}const isRequest=e=>typeof e==`object`&&typeof e.url==`string`;var _=class Request extends t{cache;credentials;destination;get headers(){return this[h].headers}integrity;keepalive;get method(){return this[h].method}mode;get redirect(){return this[h].redirect}get referrer(){if(this[h].referrer===`no-referrer`)return``;if(this[h].referrer===`client`)return`about:client`;if(this[h].referrer)return this[h].referrer.toString()}get referrerPolicy(){return this[h].referrerPolicy}set referrerPolicy(e){this[h].referrerPolicy=u(e)}get signal(){return this[h].signal}get url(){return this[h].parsedURL.toString()}get _uri(){return p.Uri.parse(this.url,p.UriFlags.ENCODED)}get _session(){return this[h].session}get _message(){return this[h].message}get _inputStream(){return this[h].inputStream}get[Symbol.toStringTag](){return`Request`}[h];follow;compress=!1;counter=0;agent=``;highWaterMark=16384;insecureHTTPParser=!1;rejectUnauthorized;constructor(t,i){let o=t,s=i||{},c,l={};if(isRequest(t)?(c=new d(o.url),l=o):c=new d(t),c.username!==``||c.password!==``)throw TypeError(`${c} is an url with embedded credentials.`);let u=s.method||l.method||`GET`;if(/^(delete|get|head|options|post|put)$/i.test(u)&&(u=u.toUpperCase()),(i?.body!=null||isRequest(t)&&o.body!==null)&&(u===`GET`||u===`HEAD`))throw TypeError(`Request with GET/HEAD method cannot have body`);let m=i?.body?i.body:isRequest(t)&&o.body!==null?n(t):null;super(m,{size:s.size||0});let g=new a(i?.headers||o.headers||{});if(m!==null&&!g.has(`Content-Type`)){let e=r(m,this);e&&g.set(`Content-Type`,e)}let _=isRequest(t)?o.signal:null;if(i&&`signal`in i&&(_=i.signal),_!=null&&!e(_))throw TypeError(`Expected signal to be an instanceof AbortSignal or EventTarget`);let v=i?.referrer==null?o.referrer:i.referrer;if(v===``)v=`no-referrer`;else if(v){let e=new d(v);v=/^about:(\/\/)?client$/.test(e.toString())?`client`:e}else v=void 0;let y=c.protocol,b=null,x=null;(y===`http:`||y===`https:`)&&(b=getSharedSession(),x=new f.Message({method:u,uri:p.Uri.parse(c.toString(),p.UriFlags.ENCODED)})),this[h]={method:u,redirect:i?.redirect||o.redirect||`follow`,headers:g,parsedURL:c,signal:_,referrer:v,referrerPolicy:``,session:b,message:x},this.follow=s.follow===void 0?o.follow===void 0?20:o.follow:s.follow,this.compress=s.compress===void 0?o.compress===void 0?!0:o.compress:s.compress,this.counter=s.counter||o.counter||0,this.agent=s.agent||o.agent,this.highWaterMark=s.highWaterMark||o.highWaterMark||16384,this.insecureHTTPParser=s.insecureHTTPParser||o.insecureHTTPParser||!1,this.rejectUnauthorized=s.rejectUnauthorized??o.rejectUnauthorized,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n,signal:r,parsedURL:i}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);e.headers._appendToSoupMessage(n);let a=this._rawBodyBuffer;if(a!==null&&a.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(a))}i.protocol===`https:`&&shouldAcceptAnyCert(this.rejectUnauthorized)&&n.connect(`accept-certificate`,(e,t,n)=>!0);let c=new m.Cancellable;return r&&(r.aborted?c.cancel():r.addEventListener(`abort`,()=>c.cancel(),{once:!0})),this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,c),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:c}}clone(){return new Request(this)}async arrayBuffer(){return super.arrayBuffer()}async blob(){return super.blob()}async formData(){return super.formData()}async json(){return super.json()}async text(){return super.text()}};Object.defineProperties(_.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0},referrer:{enumerable:!0},referrerPolicy:{enumerable:!0}});const getSoupRequestOptions=e=>{let{parsedURL:t}=e[h],n=new a(e[h].headers);n.has(`Accept`)||n.set(`Accept`,`*/*`);let r=null;if(e.body===null&&/^(post|put)$/i.test(e.method)&&(r=`0`),e.body!==null){let t=i(e);typeof t==`number`&&!Number.isNaN(t)&&(r=String(t))}r&&n.set(`Content-Length`,r),e.referrerPolicy===``&&(e.referrerPolicy=c),e.referrer&&e.referrer!==`no-referrer`?e[h].referrer=l(e):e[h].referrer=`no-referrer`,e[h].referrer instanceof d&&n.set(`Referer`,e.referrer),n.has(`User-Agent`)||n.set(`User-Agent`,`gjsify-fetch`),e.compress&&!n.has(`Accept-Encoding`)&&n.set(`Accept-Encoding`,`gzip, deflate`);let{agent:o}=e;return typeof o==`function`&&(o=o(t)),!n.has(`Connection`)&&!o&&n.set(`Connection`,`close`),{parsedURL:t,options:{headers:n}}};export{_ as Request,_ as default,getSoupRequestOptions};
1
+ import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`);let g=null;function envPositiveInt(e,t){let n=p.getenv(e);if(!n)return t;let r=Number.parseInt(n,10);return Number.isFinite(r)&&r>0?r:t}function shouldAcceptAnyCert(e){return e===!1?!0:e===!0?!1:p.getenv(`NODE_TLS_REJECT_UNAUTHORIZED`)===`0`}function getSharedSession(){if(g===null){let e=envPositiveInt(`GJSIFY_FETCH_MAX_CONNS_PER_HOST`,16),t=Math.max(envPositiveInt(`GJSIFY_FETCH_MAX_CONNS`,64),e);g=new f.Session({maxConns:t,maxConnsPerHost:e}),g.remove_feature_by_type(f.ContentDecoder.$gtype)}return g}const isRequest=e=>typeof e==`object`&&typeof e.url==`string`;var _=class Request extends t{cache;credentials;destination;get headers(){return this[h].headers}integrity;keepalive;get method(){return this[h].method}mode;get redirect(){return this[h].redirect}get referrer(){if(this[h].referrer===`no-referrer`)return``;if(this[h].referrer===`client`)return`about:client`;if(this[h].referrer)return this[h].referrer.toString()}get referrerPolicy(){return this[h].referrerPolicy}set referrerPolicy(e){this[h].referrerPolicy=u(e)}get signal(){return this[h].signal}get url(){return this[h].parsedURL.toString()}get _uri(){return p.Uri.parse(this.url,p.UriFlags.ENCODED)}get _session(){return this[h].session}get _message(){return this[h].message}get _inputStream(){return this[h].inputStream}get[Symbol.toStringTag](){return`Request`}[h];follow;compress=!1;counter=0;agent=``;highWaterMark=16384;insecureHTTPParser=!1;rejectUnauthorized;constructor(t,i){let o=t,s=i||{},c,l={};if(isRequest(t)?(c=new d(o.url),l=o):c=new d(t),c.username!==``||c.password!==``)throw TypeError(`${c} is an url with embedded credentials.`);let u=s.method||l.method||`GET`;if(/^(delete|get|head|options|post|put)$/i.test(u)&&(u=u.toUpperCase()),(i?.body!=null||isRequest(t)&&o.body!==null)&&(u===`GET`||u===`HEAD`))throw TypeError(`Request with GET/HEAD method cannot have body`);let m=i?.body?i.body:isRequest(t)&&o.body!==null?n(t):null;super(m,{size:s.size||0});let g=new a(i?.headers||o.headers||{});if(m!==null&&!g.has(`Content-Type`)){let e=r(m,this);e&&g.set(`Content-Type`,e)}let _=isRequest(t)?o.signal:null;if(i&&`signal`in i&&(_=i.signal),_!=null&&!e(_))throw TypeError(`Expected signal to be an instanceof AbortSignal or EventTarget`);let v=i?.referrer==null?o.referrer:i.referrer;if(v===``)v=`no-referrer`;else if(v){let e=new d(v);v=/^about:(\/\/)?client$/.test(e.toString())?`client`:e}else v=void 0;let y=c.protocol,b=null,x=null;(y===`http:`||y===`https:`)&&(b=getSharedSession(),x=new f.Message({method:u,uri:p.Uri.parse(c.toString(),p.UriFlags.ENCODED)})),this[h]={method:u,redirect:i?.redirect||o.redirect||`follow`,headers:g,parsedURL:c,signal:_,referrer:v,referrerPolicy:``,session:b,message:x},this.follow=s.follow===void 0?o.follow===void 0?20:o.follow:s.follow,this.compress=s.compress===void 0?o.compress===void 0?!0:o.compress:s.compress,this.counter=s.counter||o.counter||0,this.agent=s.agent||o.agent,this.highWaterMark=s.highWaterMark||o.highWaterMark||16384,this.insecureHTTPParser=s.insecureHTTPParser||o.insecureHTTPParser||!1,this.rejectUnauthorized=s.rejectUnauthorized??o.rejectUnauthorized,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n,signal:r,parsedURL:i}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);e.headers._appendToSoupMessage(n);let a=this._rawBodyBuffer;if(a!==null&&a.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(a))}i.protocol===`https:`&&shouldAcceptAnyCert(this.rejectUnauthorized)&&n.connect(`accept-certificate`,(e,t,n)=>!0);let c=new m.Cancellable;return r&&(r.aborted?c.cancel():r.addEventListener(`abort`,()=>c.cancel(),{once:!0})),this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,c),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:c}}clone(){return new Request(this)}async arrayBuffer(){return super.arrayBuffer()}async blob(){return super.blob()}async formData(){return super.formData()}async json(){return super.json()}async text(){return super.text()}};Object.defineProperties(_.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0},referrer:{enumerable:!0},referrerPolicy:{enumerable:!0}});const getSoupRequestOptions=e=>{let{parsedURL:t}=e[h],n=new a(e[h].headers);n.has(`Accept`)||n.set(`Accept`,`*/*`);let r=null;if(e.body===null&&/^(post|put)$/i.test(e.method)&&(r=`0`),e.body!==null){let t=i(e);typeof t==`number`&&!Number.isNaN(t)&&(r=String(t))}r&&n.set(`Content-Length`,r),e.referrerPolicy===``&&(e.referrerPolicy=c),e.referrer&&e.referrer!==`no-referrer`?e[h].referrer=l(e):e[h].referrer=`no-referrer`,e[h].referrer instanceof d&&n.set(`Referer`,e.referrer),n.has(`User-Agent`)||n.set(`User-Agent`,`gjsify-fetch`),e.compress&&!n.has(`Accept-Encoding`)&&n.set(`Accept-Encoding`,`gzip, deflate`);let{agent:o}=e;return typeof o==`function`&&(o=o(t)),!n.has(`Connection`)&&!o&&n.set(`Connection`,`close`),{parsedURL:t,options:{headers:n}}};export{_ as Request,_ as default,getSoupRequestOptions};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{rewriteRootRelativeUrl as e}from"./root-relative.js";import t from"system";function resolveRootRelativeUrl(n){return e(n,t.programPath??t.programInvocationName??``)}export{resolveRootRelativeUrl};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";function rewriteRootRelativeUrl(e,t){if(!e.startsWith(`/`)||e.startsWith(`//`))return e;let n=t.lastIndexOf(`/`);return n<=0?e:`file://${t.slice(0,n)}${e}`}export{rewriteRootRelativeUrl};
package/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@ import { URL } from '@gjsify/url';
10
10
  export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
11
11
  export { Blob, File };
12
12
  export { XMLHttpRequest, XMLHttpRequestUpload } from './xhr.js';
13
+ export { resolveRootRelativeUrl } from './utils/root-relative-system.js';
13
14
  /**
14
15
  * Fetch function
15
16
  *
package/lib/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // Copyright (c) node-fetch contributors. MIT license.
4
4
  // Modifications: Rewritten for GJS using libsoup 3.0 (Soup.Session)
5
5
  import GLib from '@girs/glib-2.0';
6
- import System from 'system';
7
6
  import Stream from 'node:stream';
8
7
  import { parseDataUri } from './utils/data-uri.js';
8
+ import { resolveRootRelativeUrl } from './utils/root-relative-system.js';
9
9
  import { clone } from './body.js';
10
10
  import Response from './response.js';
11
11
  import Headers from './headers.js';
@@ -21,39 +21,31 @@ import { URL } from '@gjsify/url';
21
21
  export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
22
22
  export { Blob, File };
23
23
  export { XMLHttpRequest, XMLHttpRequestUpload } from './xhr.js';
24
+ // The shared program-dir rewrite — consumed by @gjsify/xmlhttprequest so fetch
25
+ // and XHR cannot drift apart again (see utils/root-relative.ts for the pure
26
+ // logic + utils/root-relative-system.ts for the `system`-reading wrapper).
27
+ export { resolveRootRelativeUrl } from './utils/root-relative-system.js';
24
28
  const supportedSchemas = new Set(['data:', 'http:', 'https:', 'file:']);
25
- function rewriteRootRelativeUrl(input) {
29
+ /**
30
+ * String inputs go through the shared root-relative rewrite
31
+ * (`utils/root-relative.ts` — one copy for fetch AND XHR; two drifting copies
32
+ * are what #869 was). Non-string inputs pass through untouched.
33
+ */
34
+ function rewriteFetchInput(input) {
26
35
  if (typeof input !== 'string')
27
36
  return input;
28
- if (!input.startsWith('/') || input.startsWith('//'))
29
- return input;
30
- const _g = globalThis;
31
- const DEBUG = _g.__GJSIFY_DEBUG_FETCH === true;
32
- try {
33
- // Program dir from the `system` built-in — the SAME source
34
- // `@gjsify/xmlhttprequest` and `@gjsify/dom-elements` use for this
35
- // identical rewrite. It used to read `globalThis.imports.system`, which
36
- // exists only on GJS: on the node-gi reverse bridge the probe was
37
- // `undefined`, the path was handed to the runtime's native `URL`
38
- // unchanged, and every root-relative `fetch()` died with
39
- // `ERR_INVALID_URL` — while XHR-loaded assets in the same app resolved
40
- // fine, because those two siblings import the module. That asymmetry is
41
- // what made the excalibur-jelly-jumper showcase render an empty level on
42
- // node/bun/deno: its Tiled map is the one asset fetched rather than XHR'd.
43
- const programPath = System.programPath ?? System.programInvocationName ?? '';
44
- if (!programPath)
45
- return input;
46
- const dir = GLib.path_get_dirname(programPath);
47
- const rewritten = `file://${dir}${input}`;
48
- if (DEBUG)
49
- console.log(`[fetch] rewrite ${input} → ${rewritten}`);
50
- return rewritten;
51
- }
52
- catch (err) {
53
- if (DEBUG)
54
- console.warn(`[fetch] rewrite FAILED: ${err instanceof Error ? err.message : String(err)}`);
55
- return input;
37
+ // No try/catch: `resolveRootRelativeUrl` reads two properties off the
38
+ // `system` built-in and calls a pure function — there is no throw path, so
39
+ // a catch here could only ever SWALLOW a bug. That is precisely how #869
40
+ // hid: the previous spelling reached `globalThis.imports.system`, which is
41
+ // `undefined` off gjs, and the catch turned the resulting TypeError into a
42
+ // silently unrewritten URL. If this ever does throw, the caller should see
43
+ // it.
44
+ const rewritten = resolveRootRelativeUrl(input);
45
+ if (rewritten !== input && globalThis.__GJSIFY_DEBUG_FETCH === true) {
46
+ console.log(`[fetch] rewrite ${input} → ${rewritten}`);
56
47
  }
48
+ return rewritten;
57
49
  }
58
50
  /**
59
51
  * Fetch function
@@ -63,7 +55,7 @@ function rewriteRootRelativeUrl(input) {
63
55
  */
64
56
  export default async function fetch(url, init = {}) {
65
57
  // Rewrite root-relative URLs before Request constructor parses them
66
- url = rewriteRootRelativeUrl(url);
58
+ url = rewriteFetchInput(url);
67
59
  // Build request object
68
60
  const request = new Request(url, init);
69
61
  const { parsedURL, options } = getSoupRequestOptions(request);
@@ -271,7 +263,9 @@ export default async function fetch(url, init = {}) {
271
263
  // the streaming decode is fragile. Buffer first, then decompress the
272
264
  // complete in-memory blob — the same pattern @gjsify/tar uses for .tgz.
273
265
  const rawBuffer = await new Response(readable, responseOptions).arrayBuffer();
274
- const decompressed = new Blob([rawBuffer]).stream().pipeThrough(new DecompressionStream(format));
266
+ const decompressed = new Blob([rawBuffer])
267
+ .stream()
268
+ .pipeThrough(new DecompressionStream(format));
275
269
  finalize();
276
270
  return new Response(decompressed, responseOptions);
277
271
  }
@@ -36,12 +36,9 @@ if (urlCtor && urlCtor.__gjsify_objecturl !== true) {
36
36
  urlCtor.revokeObjectURL = (url) => {
37
37
  const path = objectUrlPaths.get(url);
38
38
  if (path) {
39
- try {
40
- GLib.unlink(path);
41
- }
42
- catch {
43
- // best-effort temp-file cleanup
44
- }
39
+ // Best-effort temp-file cleanup — GLib.unlink reports failure via
40
+ // its return value (-1); it has no throw path (no `throws` in GIR).
41
+ GLib.unlink(path);
45
42
  objectUrlPaths.delete(url);
46
43
  }
47
44
  else if (nativeRevoke) {
package/lib/request.js CHANGED
@@ -101,12 +101,8 @@ function getSharedSession() {
101
101
  // body without removing the Content-Encoding header, causing
102
102
  // double-decompression when index.ts also runs DecompressionStream.
103
103
  // Remove it once here so our JS-level decompression handles everything.
104
- try {
105
- sharedSession.remove_feature_by_type(Soup.ContentDecoder.$gtype);
106
- }
107
- catch {
108
- /* not present */
109
- }
104
+ // (No throw path: an absent feature is a documented silent no-op.)
105
+ sharedSession.remove_feature_by_type(Soup.ContentDecoder.$gtype);
110
106
  }
111
107
  return sharedSession;
112
108
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,56 @@
1
+ // Coverage for the SHARED root-relative rewrite (utils/root-relative.ts).
2
+ //
3
+ // The regression it pins: fetch and XHR used to carry two hand-written copies
4
+ // of this rewrite, and the fetch copy drifted (reading the GJS-only
5
+ // `globalThis.imports.system`) — every root-relative `fetch()` then died with
6
+ // `ERR_INVALID_URL` on the node-gi reverse bridge while XHR kept working
7
+ // (#869). One function, consumed by both, tested here on gjs AND node.
8
+ //
9
+ // The PURE half (`rewriteRootRelativeUrl(url, programPath)`) is imported
10
+ // statically and covered on BOTH runtimes. The `system`-reading wrapper
11
+ // (`utils/root-relative-system.ts`) is deliberately NOT imported statically:
12
+ // on `--app node` the bare `system` built-in is aliased to
13
+ // `@gjsify/node-gi/system` and kept external, so a static import here would
14
+ // make the node test bundle hard-require the node-gi bridge — the exact
15
+ // cross-platform loadability this split exists to protect. The wrapper is
16
+ // instead exercised on gjs via the aliased package root (`import('fetch')`),
17
+ // which on node resolves to the harmless native-globals re-export.
18
+ import { describe, expect, it, on } from '@gjsify/unit';
19
+ import { rewriteRootRelativeUrl } from './utils/root-relative.js';
20
+ export default async () => {
21
+ await describe('fetch.rewriteRootRelativeUrl (pure)', async () => {
22
+ await it('maps a root-relative URL to file:// under the program dir', async () => {
23
+ expect(rewriteRootRelativeUrl('/res/tilemaps/level1.tmx', '/opt/app/dist/main.js')).toBe('file:///opt/app/dist/res/tilemaps/level1.tmx');
24
+ });
25
+ await it('leaves a protocol-relative URL (//host/...) untouched', async () => {
26
+ expect(rewriteRootRelativeUrl('//cdn.example.com/x.png', '/opt/app/dist/main.js')).toBe('//cdn.example.com/x.png');
27
+ });
28
+ await it('leaves absolute and relative URLs untouched', async () => {
29
+ expect(rewriteRootRelativeUrl('https://example.com/a', '/opt/app/dist/main.js')).toBe('https://example.com/a');
30
+ expect(rewriteRootRelativeUrl('res/local.png', '/opt/app/dist/main.js')).toBe('res/local.png');
31
+ });
32
+ await it('returns the input when no usable program dir exists', async () => {
33
+ // '' (no program path at all) and a bare name (no directory part).
34
+ expect(rewriteRootRelativeUrl('/res/x.png', '')).toBe('/res/x.png');
35
+ expect(rewriteRootRelativeUrl('/res/x.png', 'main.js')).toBe('/res/x.png');
36
+ });
37
+ });
38
+ // The `system`-reading wrapper, end to end on gjs — the runtime where the
39
+ // program path is real. Reached through the aliased package root so the
40
+ // node test bundle never links the `system` external (see file header).
41
+ await on('Gjs', async () => {
42
+ await describe('fetch.resolveRootRelativeUrl (system wrapper)', async () => {
43
+ await it('maps a root-relative URL to file:// under the real program dir', async () => {
44
+ // Namespace access (not a destructured binding): on the node
45
+ // build this module is the globals re-export without the
46
+ // wrapper, and only the gjs path ever reaches this line.
47
+ const fetchModule = await import('fetch');
48
+ const rewritten = fetchModule.resolveRootRelativeUrl('/res/tilemaps/level1.tmx');
49
+ // The program path is the test bundle's own path — assert the
50
+ // CONTRACT, not the host.
51
+ expect(rewritten.startsWith('file:///')).toBe(true);
52
+ expect(rewritten.endsWith('/res/tilemaps/level1.tmx')).toBe(true);
53
+ });
54
+ });
55
+ });
56
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Rewrite a root-relative URL to `file://` under the running program's
3
+ * directory (see `rewriteRootRelativeUrl` for the contract). Reads the program
4
+ * path from the `system` built-in — `programPath` on gjs, with
5
+ * `programInvocationName` as the fallback (on the node bridge that is
6
+ * `process.argv[1]`, the bundle itself — the right base).
7
+ */
8
+ export declare function resolveRootRelativeUrl(url: string): string;
@@ -0,0 +1,25 @@
1
+ // The GJS-host binding of the shared root-relative rewrite: reads the running
2
+ // program's path from the `system` built-in and delegates to the PURE
3
+ // `rewriteRootRelativeUrl` (utils/root-relative.ts — the #869 single copy).
4
+ //
5
+ // This wrapper is a SEPARATE module on purpose. The bare `system` built-in is
6
+ // external on the gjs target and aliased to `@gjsify/node-gi/system` (kept
7
+ // external) on `--app node`, so whichever module imports it drags a hard
8
+ // node-gi requirement into the emitted bundle. Keeping the import here confines
9
+ // that to the GJS-reachable path (fetch's own Soup-backed impl + XHR), while
10
+ // the pure half stays testable — and loadable — on plain Node.
11
+ // The bare `system` built-in — NOT `globalThis.imports.system`: the probe shape
12
+ // is `undefined` on the node-gi reverse bridge unless the globals shim happens
13
+ // to be injected, which is exactly how #869's silent `ERR_INVALID_URL` hid.
14
+ import System from 'system';
15
+ import { rewriteRootRelativeUrl } from './root-relative.js';
16
+ /**
17
+ * Rewrite a root-relative URL to `file://` under the running program's
18
+ * directory (see `rewriteRootRelativeUrl` for the contract). Reads the program
19
+ * path from the `system` built-in — `programPath` on gjs, with
20
+ * `programInvocationName` as the fallback (on the node bridge that is
21
+ * `process.argv[1]`, the bundle itself — the right base).
22
+ */
23
+ export function resolveRootRelativeUrl(url) {
24
+ return rewriteRootRelativeUrl(url, System.programPath ?? System.programInvocationName ?? '');
25
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Rewrite a root-relative URL (starts with `/` but not `//`, e.g.
3
+ * `/res/images/foo.png`) to a `file://` URL under the given program path's
4
+ * directory, so a GJS/GTK app loads bundled assets with the same paths a
5
+ * browser would resolve against its origin.
6
+ *
7
+ * Returns the input unchanged when it is not root-relative or when no usable
8
+ * program path is given. The security implications (arbitrary file reads) are
9
+ * acceptable for the current use cases — revisit if these packages ever handle
10
+ * untrusted input.
11
+ *
12
+ * Deliberately no `GLib.path_get_dirname`: `@girs/glib-2.0` is emptied to `{}`
13
+ * in a plain-Node bundle (only the gjs target and the reverse bridge resolve
14
+ * it), so a GLib call here only ever "worked" behind a swallowing catch. The
15
+ * program path is `/`-separated on every runtime this rewrite serves, making
16
+ * dirname a string slice.
17
+ *
18
+ * @param url The URL to rewrite.
19
+ * @param programPath The running program's path (`System.programPath ??
20
+ * System.programInvocationName` on gjs; `process.argv[1]` — the bundle
21
+ * itself — on the node-gi reverse bridge).
22
+ */
23
+ export declare function rewriteRootRelativeUrl(url: string, programPath: string): string;
@@ -0,0 +1,49 @@
1
+ // Root-relative URL → `file://` under the program directory.
2
+ //
3
+ // THE one copy of this rewrite. `@gjsify/fetch` and `@gjsify/xmlhttprequest`
4
+ // used to carry two hand-written spellings of it, and the copies drifting is
5
+ // exactly what broke: the fetch copy read `globalThis.imports.system` (GJS-only)
6
+ // while the XHR copy imported the `system` built-in, so on the node-gi reverse
7
+ // bridge every XHR-loaded asset resolved and every root-relative `fetch()` died
8
+ // with `ERR_INVALID_URL` (#869 — the excalibur-jelly-jumper showcase rendered an
9
+ // empty level because its Tiled map is the one asset fetched rather than XHR'd).
10
+ //
11
+ // This module is deliberately PURE — the program path is a PARAMETER, and the
12
+ // `system` built-in is read one module over (`root-relative-system.ts`). The
13
+ // split is load-bearing for cross-platform loadability: on `--app node` the
14
+ // bare `system` specifier is aliased to `@gjsify/node-gi/system` and kept
15
+ // EXTERNAL, so any module that imports it makes the emitted bundle hard-require
16
+ // the node-gi bridge at load. `@gjsify/fetch` is a cross-platform package whose
17
+ // node test bundle must load on plain Node — its spec therefore covers THIS
18
+ // pure function on gjs AND node, while the `system` read stays confined to the
19
+ // GJS-reachable wrapper.
20
+ /**
21
+ * Rewrite a root-relative URL (starts with `/` but not `//`, e.g.
22
+ * `/res/images/foo.png`) to a `file://` URL under the given program path's
23
+ * directory, so a GJS/GTK app loads bundled assets with the same paths a
24
+ * browser would resolve against its origin.
25
+ *
26
+ * Returns the input unchanged when it is not root-relative or when no usable
27
+ * program path is given. The security implications (arbitrary file reads) are
28
+ * acceptable for the current use cases — revisit if these packages ever handle
29
+ * untrusted input.
30
+ *
31
+ * Deliberately no `GLib.path_get_dirname`: `@girs/glib-2.0` is emptied to `{}`
32
+ * in a plain-Node bundle (only the gjs target and the reverse bridge resolve
33
+ * it), so a GLib call here only ever "worked" behind a swallowing catch. The
34
+ * program path is `/`-separated on every runtime this rewrite serves, making
35
+ * dirname a string slice.
36
+ *
37
+ * @param url The URL to rewrite.
38
+ * @param programPath The running program's path (`System.programPath ??
39
+ * System.programInvocationName` on gjs; `process.argv[1]` — the bundle
40
+ * itself — on the node-gi reverse bridge).
41
+ */
42
+ export function rewriteRootRelativeUrl(url, programPath) {
43
+ if (!url.startsWith('/') || url.startsWith('//'))
44
+ return url;
45
+ const slash = programPath.lastIndexOf('/');
46
+ if (slash <= 0)
47
+ return url; // no usable program dir ('' or a bare name)
48
+ return `file://${programPath.slice(0, slash)}${url}`;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.25.1",
3
+ "version": "0.26.1",
4
4
  "description": "Web and Node.js fetch module for Gjs",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -51,8 +51,8 @@
51
51
  "fetch"
52
52
  ],
53
53
  "devDependencies": {
54
- "@gjsify/cli": "^0.25.1",
55
- "@gjsify/unit": "^0.25.1",
54
+ "@gjsify/cli": "^0.26.1",
55
+ "@gjsify/unit": "^0.26.1",
56
56
  "@types/node": "^25.9.2",
57
57
  "typescript": "^6.0.3"
58
58
  },
@@ -61,11 +61,11 @@
61
61
  "@girs/gjs": "^4.1.0",
62
62
  "@girs/glib-2.0": "^4.1.0",
63
63
  "@girs/soup-3.0": "^4.1.0",
64
- "@gjsify/dom-events": "^0.25.1",
65
- "@gjsify/formdata": "^0.25.1",
66
- "@gjsify/http": "^0.25.1",
67
- "@gjsify/url": "^0.25.1",
68
- "@gjsify/utils": "^0.25.1"
64
+ "@gjsify/dom-events": "^0.26.1",
65
+ "@gjsify/formdata": "^0.26.1",
66
+ "@gjsify/http": "^0.26.1",
67
+ "@gjsify/url": "^0.26.1",
68
+ "@gjsify/utils": "^0.26.1"
69
69
  },
70
70
  "gjsify": {
71
71
  "runtimes": {