@gjsify/fetch 0.35.0 → 0.37.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.
package/lib/body.d.ts CHANGED
@@ -19,6 +19,8 @@ export default class Body {
19
19
  [INTERNALS]: {
20
20
  body: null | Buffer | Readable | Blob;
21
21
  stream: Readable | null;
22
+ /** Memoized `body` wrapper — see the getter for why identity matters. */
23
+ webStream: ReadableStream<Uint8Array> | null;
22
24
  boundary: string;
23
25
  disturbed: boolean;
24
26
  error: null | FetchBaseError;
@@ -28,6 +30,22 @@ export default class Body {
28
30
  size?: number;
29
31
  headers?: unknown;
30
32
  });
33
+ /**
34
+ * The body as a `ReadableStream`.
35
+ *
36
+ * Per the Fetch spec `body` is an ATTRIBUTE holding one stream, so every
37
+ * read must return the SAME object — `res.body === res.body`. This used to
38
+ * build a fresh wrapper per access, each one subscribing to the same
39
+ * already-flowing `Readable`: the first reader got every byte and every
40
+ * later one got an immediate `done`, with no error anywhere.
41
+ *
42
+ * That is not a theoretical spec detail. Consumers legitimately touch
43
+ * `.body` several times in a row — three.js' `FileLoader` guards with
44
+ * `response.body === undefined`, then `response.body.getReader === undefined`,
45
+ * and only then calls `response.body.getReader()`, so it read the THIRD,
46
+ * empty stream. Downstream: an empty response text, an LDraw model that
47
+ * parsed to zero meshes, and a viewer showing nothing but its clear colour.
48
+ */
31
49
  get body(): ReadableStream<Uint8Array> | null;
32
50
  get _stream(): Readable;
33
51
  /** Return the raw body buffer without consuming the stream (used by Request._send). */
package/lib/body.js CHANGED
@@ -34,6 +34,7 @@ export default class Body {
34
34
  [INTERNALS] = {
35
35
  body: null,
36
36
  stream: null,
37
+ webStream: null,
37
38
  boundary: '',
38
39
  disturbed: false,
39
40
  error: null,
@@ -110,14 +111,33 @@ export default class Body {
110
111
  });
111
112
  }
112
113
  }
114
+ /**
115
+ * The body as a `ReadableStream`.
116
+ *
117
+ * Per the Fetch spec `body` is an ATTRIBUTE holding one stream, so every
118
+ * read must return the SAME object — `res.body === res.body`. This used to
119
+ * build a fresh wrapper per access, each one subscribing to the same
120
+ * already-flowing `Readable`: the first reader got every byte and every
121
+ * later one got an immediate `done`, with no error anywhere.
122
+ *
123
+ * That is not a theoretical spec detail. Consumers legitimately touch
124
+ * `.body` several times in a row — three.js' `FileLoader` guards with
125
+ * `response.body === undefined`, then `response.body.getReader === undefined`,
126
+ * and only then calls `response.body.getReader()`, so it read the THIRD,
127
+ * empty stream. Downstream: an empty response text, an LDraw model that
128
+ * parsed to zero meshes, and a viewer showing nothing but its clear colour.
129
+ */
113
130
  get body() {
114
131
  const stream = this[INTERNALS].stream;
115
132
  if (!stream)
116
133
  return null;
134
+ const memoized = this[INTERNALS].webStream;
135
+ if (memoized)
136
+ return memoized;
117
137
  // If ReadableStream is available, wrap the Readable into one
118
138
  if (typeof ReadableStream !== 'undefined') {
119
139
  let closed = false;
120
- return new ReadableStream({
140
+ const webStream = new ReadableStream({
121
141
  start(controller) {
122
142
  stream.on('data', (chunk) => {
123
143
  if (closed)
@@ -160,6 +180,8 @@ export default class Body {
160
180
  stream.destroy();
161
181
  },
162
182
  });
183
+ this[INTERNALS].webStream = webStream;
184
+ return webStream;
163
185
  }
164
186
  return null;
165
187
  }
package/lib/esm/body.js CHANGED
@@ -1 +1 @@
1
- import{__name as e}from"./_virtual/_rolldown/runtime.js";import{Blob as t}from"./utils/blob-from.js";import{FetchBaseError as n}from"./errors/base.js";import{FetchError as r}from"./errors/fetch-error.js";import{isBlob as i,isURLSearchParameters as a}from"./utils/is.js";import{URLSearchParams as o}from"@gjsify/url";import{Buffer as s}from"node:buffer";import{PassThrough as c,Readable as l,Stream as u,pipeline as d}from"node:stream";import{FormData as f,formDataToBlob as p}from"@gjsify/formdata";const m=e((e,t)=>new Promise((n,r)=>{d(e,t,e=>{e?r(e):n()})}),`pipeline`),h=Symbol(`Body internals`);function isAnyArrayBuffer(e){return e instanceof ArrayBuffer||typeof SharedArrayBuffer<`u`&&e instanceof SharedArrayBuffer}var Body=class{[h]={body:null,stream:null,boundary:``,disturbed:!1,error:null};size=0;constructor(e,t={size:0}){if(this.size=t.size||0,e==null)this[h].body=null;else if(a(e))this[h].body=s.from(e.toString());else if(i(e))this[h].body=e;else if(s.isBuffer(e))this[h].body=e;else if(isAnyArrayBuffer(e))this[h].body=s.from(e);else if(ArrayBuffer.isView(e))this[h].body=s.from(e.buffer,e.byteOffset,e.byteLength);else if(e instanceof l)this[h].body=e;else if(typeof ReadableStream<`u`&&e instanceof ReadableStream)this[h].body=readableStreamToReadable(e);else if(e instanceof f){let t=p(e);this[h].body=t,this[h].boundary=t.type?.split(`boundary=`)?.[1]??``}else typeof e==`string`?this[h].body=s.from(e):e instanceof o?this[h].body=s.from(e.toString()):(console.warn(`Unknown body type "${typeof e}", try to parse the body to string!`),this[h].body=s.from(String(e)));let c=this[h].body;s.isBuffer(c)?this[h].stream=l.from(c):i(c)?this[h].stream=l.from(blobToAsyncIterable(c)):c instanceof l&&(this[h].stream=c),c instanceof u&&c.on(`error`,e=>{let t=e instanceof n?e:new r(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,`system`,e);this[h].error=t})}get body(){let e=this[h].stream;if(!e)return null;if(typeof ReadableStream<`u`){let t=!1;return new ReadableStream({start(n){e.on(`data`,e=>{if(!t)try{n.enqueue(e instanceof Uint8Array?e:new Uint8Array(e))}catch{}}),e.on(`end`,()=>{if(!t){t=!0;try{n.close()}catch{}}}),e.on(`error`,e=>{if(!t){t=!0;try{n.error(e)}catch{}}})},cancel(){t=!0,e.destroy()}})}return null}get _stream(){return this[h].stream}get _rawBodyBuffer(){let e=this[h].body;return e===null?null:s.isBuffer(e)?e:e instanceof Uint8Array?s.from(e.buffer,e.byteOffset,e.byteLength):null}get bodyUsed(){return this[h].disturbed}async arrayBuffer(){let{buffer:e,byteOffset:t,byteLength:n}=await consumeBody(this);return e.slice(t,t+n)}async formData(){let e=this.headers?.get(`content-type`);if(e?.startsWith(`application/x-www-form-urlencoded`)){let e=new f,t=new o(await this.text());for(let[n,r]of t)e.append(n,r);return e}let{toFormData:t}=await import(`./utils/multipart-parser.js`);return t(this.body,e)}async blob(){let e=this.headers?.get(`content-type`)||this[h].body&&this[h].body.type||``;return new t([await this.arrayBuffer()],{type:e})}async json(){let e=await this.text();return JSON.parse(e)}async text(){let e=await consumeBody(this);return new TextDecoder().decode(e)}};Object.defineProperties(Body.prototype,{body:{enumerable:!0},bodyUsed:{enumerable:!0},arrayBuffer:{enumerable:!0},blob:{enumerable:!0},json:{enumerable:!0},text:{enumerable:!0}});async function consumeBody(e){if(e[h].disturbed)throw TypeError(`body used already for: ${e.url}`);if(e[h].disturbed=!0,e[h].error)throw e[h].error;let{_stream:t}=e;if(t===null||!(t instanceof u))return s.alloc(0);let i=[],a=0;try{for await(let n of t){if(e.size>0&&a+n.length>e.size){let n=new r(`content size at ${e.url} over limit: ${e.size}`,`max-size`);throw t.destroy(n),n}a+=n.length,i.push(n)}}catch(t){let i=t instanceof Error?t:Error(String(t));throw t instanceof n?t:new r(`Invalid response body while trying to fetch ${e.url}: ${i.message}`,`system`,i)}try{return i.every(e=>typeof e==`string`)?s.from(i.join(``)):s.concat(i,a)}catch(t){let n=t instanceof Error?t:Error(String(t));throw new r(`Could not create Buffer from response body for ${e.url}: ${n.message}`,`system`,n)}}const clone=(e,t)=>{let n,r,{body:i}=e[h];if(e.bodyUsed)throw Error(`cannot clone body after it is used`);return i instanceof u&&typeof i.getBoundary!=`function`&&(n=new c({highWaterMark:t}),r=new c({highWaterMark:t}),i.pipe(n),i.pipe(r),e[h].stream=n,i=r),i},extractContentType=(e,t)=>e===null?null:typeof e==`string`?`text/plain;charset=UTF-8`:a(e)?`application/x-www-form-urlencoded;charset=UTF-8`:i(e)?e.type||null:s.isBuffer(e)||isAnyArrayBuffer(e)||ArrayBuffer.isView(e)?null:e instanceof f?`multipart/form-data; boundary=${t[h].boundary}`:e instanceof u?null:`text/plain;charset=UTF-8`,getTotalBytes=e=>{let{body:t}=e[h];if(t===null)return 0;if(i(t))return t.size;if(s.isBuffer(t))return t.length;if(t&&typeof t.getLengthSync==`function`){let e=t;return e.hasKnownLength&&e.hasKnownLength()?e.getLengthSync():null}return null},writeToStream=async(e,{body:t})=>{t===null?e.end():await m(t,e)};function readableStreamToReadable(e){let t=e.getReader();return new l({async read(){try{let{done:e,value:n}=await t.read();e?this.push(null):this.push(s.from(n))}catch(e){this.destroy(e)}},destroy(e,n){t.cancel().then(()=>n(null),n)}})}async function*blobToAsyncIterable(e){if(typeof e.stream==`function`){let t=e.stream().getReader();for(;;){let{done:e,value:n}=await t.read();if(e)break;yield n}}else yield new Uint8Array(await e.arrayBuffer())}export{clone,Body as default,extractContentType,getTotalBytes,writeToStream};
1
+ import{__name as e}from"./_virtual/_rolldown/runtime.js";import{Blob as t}from"./utils/blob-from.js";import{FetchBaseError as n}from"./errors/base.js";import{FetchError as r}from"./errors/fetch-error.js";import{isBlob as i,isURLSearchParameters as a}from"./utils/is.js";import{URLSearchParams as o}from"@gjsify/url";import{Buffer as s}from"node:buffer";import{PassThrough as c,Readable as l,Stream as u,pipeline as d}from"node:stream";import{FormData as f,formDataToBlob as p}from"@gjsify/formdata";const m=e((e,t)=>new Promise((n,r)=>{d(e,t,e=>{e?r(e):n()})}),`pipeline`),h=Symbol(`Body internals`);function isAnyArrayBuffer(e){return e instanceof ArrayBuffer||typeof SharedArrayBuffer<`u`&&e instanceof SharedArrayBuffer}var Body=class{[h]={body:null,stream:null,webStream:null,boundary:``,disturbed:!1,error:null};size=0;constructor(e,t={size:0}){if(this.size=t.size||0,e==null)this[h].body=null;else if(a(e))this[h].body=s.from(e.toString());else if(i(e))this[h].body=e;else if(s.isBuffer(e))this[h].body=e;else if(isAnyArrayBuffer(e))this[h].body=s.from(e);else if(ArrayBuffer.isView(e))this[h].body=s.from(e.buffer,e.byteOffset,e.byteLength);else if(e instanceof l)this[h].body=e;else if(typeof ReadableStream<`u`&&e instanceof ReadableStream)this[h].body=readableStreamToReadable(e);else if(e instanceof f){let t=p(e);this[h].body=t,this[h].boundary=t.type?.split(`boundary=`)?.[1]??``}else typeof e==`string`?this[h].body=s.from(e):e instanceof o?this[h].body=s.from(e.toString()):(console.warn(`Unknown body type "${typeof e}", try to parse the body to string!`),this[h].body=s.from(String(e)));let c=this[h].body;s.isBuffer(c)?this[h].stream=l.from(c):i(c)?this[h].stream=l.from(blobToAsyncIterable(c)):c instanceof l&&(this[h].stream=c),c instanceof u&&c.on(`error`,e=>{let t=e instanceof n?e:new r(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,`system`,e);this[h].error=t})}get body(){let e=this[h].stream;if(!e)return null;let t=this[h].webStream;if(t)return t;if(typeof ReadableStream<`u`){let t=!1,n=new ReadableStream({start(n){e.on(`data`,e=>{if(!t)try{n.enqueue(e instanceof Uint8Array?e:new Uint8Array(e))}catch{}}),e.on(`end`,()=>{if(!t){t=!0;try{n.close()}catch{}}}),e.on(`error`,e=>{if(!t){t=!0;try{n.error(e)}catch{}}})},cancel(){t=!0,e.destroy()}});return this[h].webStream=n,n}return null}get _stream(){return this[h].stream}get _rawBodyBuffer(){let e=this[h].body;return e===null?null:s.isBuffer(e)?e:e instanceof Uint8Array?s.from(e.buffer,e.byteOffset,e.byteLength):null}get bodyUsed(){return this[h].disturbed}async arrayBuffer(){let{buffer:e,byteOffset:t,byteLength:n}=await consumeBody(this);return e.slice(t,t+n)}async formData(){let e=this.headers?.get(`content-type`);if(e?.startsWith(`application/x-www-form-urlencoded`)){let e=new f,t=new o(await this.text());for(let[n,r]of t)e.append(n,r);return e}let{toFormData:t}=await import(`./utils/multipart-parser.js`);return t(this.body,e)}async blob(){let e=this.headers?.get(`content-type`)||this[h].body&&this[h].body.type||``;return new t([await this.arrayBuffer()],{type:e})}async json(){let e=await this.text();return JSON.parse(e)}async text(){let e=await consumeBody(this);return new TextDecoder().decode(e)}};Object.defineProperties(Body.prototype,{body:{enumerable:!0},bodyUsed:{enumerable:!0},arrayBuffer:{enumerable:!0},blob:{enumerable:!0},json:{enumerable:!0},text:{enumerable:!0}});async function consumeBody(e){if(e[h].disturbed)throw TypeError(`body used already for: ${e.url}`);if(e[h].disturbed=!0,e[h].error)throw e[h].error;let{_stream:t}=e;if(t===null||!(t instanceof u))return s.alloc(0);let i=[],a=0;try{for await(let n of t){if(e.size>0&&a+n.length>e.size){let n=new r(`content size at ${e.url} over limit: ${e.size}`,`max-size`);throw t.destroy(n),n}a+=n.length,i.push(n)}}catch(t){let i=t instanceof Error?t:Error(String(t));throw t instanceof n?t:new r(`Invalid response body while trying to fetch ${e.url}: ${i.message}`,`system`,i)}try{return i.every(e=>typeof e==`string`)?s.from(i.join(``)):s.concat(i,a)}catch(t){let n=t instanceof Error?t:Error(String(t));throw new r(`Could not create Buffer from response body for ${e.url}: ${n.message}`,`system`,n)}}const clone=(e,t)=>{let n,r,{body:i}=e[h];if(e.bodyUsed)throw Error(`cannot clone body after it is used`);return i instanceof u&&typeof i.getBoundary!=`function`&&(n=new c({highWaterMark:t}),r=new c({highWaterMark:t}),i.pipe(n),i.pipe(r),e[h].stream=n,i=r),i},extractContentType=(e,t)=>e===null?null:typeof e==`string`?`text/plain;charset=UTF-8`:a(e)?`application/x-www-form-urlencoded;charset=UTF-8`:i(e)?e.type||null:s.isBuffer(e)||isAnyArrayBuffer(e)||ArrayBuffer.isView(e)?null:e instanceof f?`multipart/form-data; boundary=${t[h].boundary}`:e instanceof u?null:`text/plain;charset=UTF-8`,getTotalBytes=e=>{let{body:t}=e[h];if(t===null)return 0;if(i(t))return t.size;if(s.isBuffer(t))return t.length;if(t&&typeof t.getLengthSync==`function`){let e=t;return e.hasKnownLength&&e.hasKnownLength()?e.getLengthSync():null}return null},writeToStream=async(e,{body:t})=>{t===null?e.end():await m(t,e)};function readableStreamToReadable(e){let t=e.getReader();return new l({async read(){try{let{done:e,value:n}=await t.read();e?this.push(null):this.push(s.from(n))}catch(e){this.destroy(e)}},destroy(e,n){t.cancel().then(()=>n(null),n)}})}async function*blobToAsyncIterable(e){if(typeof e.stream==`function`){let t=e.stream().getReader();for(;;){let{done:e,value:n}=await t.read();if(e)break;yield n}}else yield new Uint8Array(await e.arrayBuffer())}export{clone,Body as default,extractContentType,getTotalBytes,writeToStream};
package/lib/index.spec.js CHANGED
@@ -197,6 +197,38 @@ export default async () => {
197
197
  await r.text();
198
198
  expect(r.bodyUsed).toBe(true);
199
199
  });
200
+ // `body` is an ATTRIBUTE holding one stream, not a factory. Reading it
201
+ // twice used to build two wrappers over the same flowing Readable, so
202
+ // the first reader drained everything and every later one saw an
203
+ // immediate `done` — silently, with no error. three.js' FileLoader
204
+ // reads `.body` three times in its feature guard before taking a
205
+ // reader, and so received an empty document.
206
+ await it('should return the same body stream on every read', async () => {
207
+ const r = new Response('stable body');
208
+ expect(r.body).toBe(r.body);
209
+ });
210
+ await it('should deliver the full body to a reader taken after re-reading body', async () => {
211
+ const r = new Response('the whole payload');
212
+ // Touch `.body` twice first, exactly like a feature guard does.
213
+ expect(r.body !== undefined).toBe(true);
214
+ expect(typeof r.body?.getReader).toBe('function');
215
+ const reader = r.body.getReader();
216
+ const chunks = [];
217
+ for (;;) {
218
+ const { done, value } = await reader.read();
219
+ if (done)
220
+ break;
221
+ chunks.push(value);
222
+ }
223
+ const total = chunks.reduce((n, c) => n + c.byteLength, 0);
224
+ const joined = new Uint8Array(total);
225
+ let offset = 0;
226
+ for (const c of chunks) {
227
+ joined.set(c, offset);
228
+ offset += c.byteLength;
229
+ }
230
+ expect(new TextDecoder().decode(joined)).toBe('the whole payload');
231
+ });
200
232
  await it('Response.error() should return error response', async () => {
201
233
  const r = Response.error();
202
234
  expect(r.type).toBe('error');
@@ -1,23 +1,17 @@
1
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.
2
+ * Rewrite a root-relative URL (starts with `/` but not `//`) to a `file://` URL under the
3
+ * program path's directory, so a GJS/GTK app loads bundled assets with the same paths a
4
+ * browser resolves against its origin. Returns the input unchanged when it is not
5
+ * root-relative or no usable program path is given.
6
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.
7
+ * The security implications (arbitrary file reads) are acceptable for the current use
8
+ * cases — revisit if these packages ever handle untrusted input.
11
9
  *
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.
10
+ * Deliberately no `GLib.path_get_dirname`: `@girs/glib-2.0` is emptied to `{}` in a
11
+ * plain-Node bundle, so a GLib call here only ever "worked" behind a swallowing catch. The
12
+ * program path is `/`-separated on every runtime this serves, making dirname a slice.
17
13
  *
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).
14
+ * @param programPath `System.programPath ?? System.programInvocationName` on gjs;
15
+ * `process.argv[1]` — the bundle itself — on the node-gi reverse bridge.
22
16
  */
23
17
  export declare function rewriteRootRelativeUrl(url: string, programPath: string): string;
@@ -1,43 +1,29 @@
1
- // Root-relative URL → `file://` under the program directory.
1
+ // Root-relative URL → `file://` under the program directory. THE one copy: `@gjsify/fetch`
2
+ // and `@gjsify/xmlhttprequest` used to carry two spellings, and they drifted — the fetch
3
+ // copy read `globalThis.imports.system` (GJS-only) while the XHR copy imported the `system`
4
+ // built-in, so on the node-gi reverse bridge XHR-loaded assets resolved and every
5
+ // root-relative `fetch()` died with `ERR_INVALID_URL` (#869).
2
6
  //
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.
7
+ // Deliberately PURE — the program path is a PARAMETER and the `system` built-in is read one
8
+ // module over (`root-relative-system.ts`). The split is load-bearing: on `--app node` the
9
+ // bare `system` specifier is aliased to `@gjsify/node-gi/system` and kept EXTERNAL, so any
10
+ // module importing it makes the bundle hard-require the bridge at load, and this package's
11
+ // node test bundle must load on plain Node.
20
12
  /**
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.
13
+ * Rewrite a root-relative URL (starts with `/` but not `//`) to a `file://` URL under the
14
+ * program path's directory, so a GJS/GTK app loads bundled assets with the same paths a
15
+ * browser resolves against its origin. Returns the input unchanged when it is not
16
+ * root-relative or no usable program path is given.
25
17
  *
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.
18
+ * The security implications (arbitrary file reads) are acceptable for the current use
19
+ * cases — revisit if these packages ever handle untrusted input.
30
20
  *
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.
21
+ * Deliberately no `GLib.path_get_dirname`: `@girs/glib-2.0` is emptied to `{}` in a
22
+ * plain-Node bundle, so a GLib call here only ever "worked" behind a swallowing catch. The
23
+ * program path is `/`-separated on every runtime this serves, making dirname a slice.
36
24
  *
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).
25
+ * @param programPath `System.programPath ?? System.programInvocationName` on gjs;
26
+ * `process.argv[1]` — the bundle itself — on the node-gi reverse bridge.
41
27
  */
42
28
  export function rewriteRootRelativeUrl(url, programPath) {
43
29
  if (!url.startsWith('/') || url.startsWith('//'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
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.35.0",
55
- "@gjsify/unit": "^0.35.0",
54
+ "@gjsify/cli": "^0.37.0",
55
+ "@gjsify/unit": "^0.37.0",
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.35.0",
65
- "@gjsify/formdata": "^0.35.0",
66
- "@gjsify/http": "^0.35.0",
67
- "@gjsify/url": "^0.35.0",
68
- "@gjsify/utils": "^0.35.0"
64
+ "@gjsify/dom-events": "^0.37.0",
65
+ "@gjsify/formdata": "^0.37.0",
66
+ "@gjsify/http": "^0.37.0",
67
+ "@gjsify/url": "^0.37.0",
68
+ "@gjsify/utils": "^0.37.0"
69
69
  },
70
70
  "gjsify": {
71
71
  "runtimes": {