goscript 0.2.2 → 0.2.4

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.
Files changed (160) hide show
  1. package/compiler/gotest/testdata/browserapi/browserapi_test.go +36 -0
  2. package/compiler/lowering.go +279 -16
  3. package/compiler/override-registry_test.go +175 -0
  4. package/compiler/protobuf-ts-binding.go +154 -6
  5. package/compiler/protobuf-ts-binding_test.go +7 -2
  6. package/compiler/runtime-contract.go +2 -0
  7. package/compiler/runtime-contract_test.go +1 -0
  8. package/compiler/semantic-model.go +16 -0
  9. package/compiler/semantic-model_test.go +38 -0
  10. package/compiler/skeleton_test.go +522 -17
  11. package/compiler/typescript-emitter.go +4 -0
  12. package/dist/gs/builtin/builtin.js +7 -9
  13. package/dist/gs/builtin/builtin.js.map +1 -1
  14. package/dist/gs/builtin/defer.js +2 -2
  15. package/dist/gs/builtin/hostio.js +5 -5
  16. package/dist/gs/builtin/hostio.js.map +1 -1
  17. package/dist/gs/builtin/map.js +2 -1
  18. package/dist/gs/builtin/map.js.map +1 -1
  19. package/dist/gs/builtin/slice.d.ts +3 -0
  20. package/dist/gs/builtin/slice.js +39 -0
  21. package/dist/gs/builtin/slice.js.map +1 -1
  22. package/dist/gs/builtin/type.js +49 -0
  23. package/dist/gs/builtin/type.js.map +1 -1
  24. package/dist/gs/compress/gzip/index.d.ts +41 -0
  25. package/dist/gs/compress/gzip/index.js +235 -0
  26. package/dist/gs/compress/gzip/index.js.map +1 -0
  27. package/dist/gs/compress/zlib/index.js +5 -2
  28. package/dist/gs/compress/zlib/index.js.map +1 -1
  29. package/dist/gs/crypto/ecdh/index.js +27 -8
  30. package/dist/gs/crypto/ecdh/index.js.map +1 -1
  31. package/dist/gs/crypto/ed25519/index.js +3 -3
  32. package/dist/gs/crypto/ed25519/index.js.map +1 -1
  33. package/dist/gs/crypto/rand/index.js +6 -3
  34. package/dist/gs/crypto/rand/index.js.map +1 -1
  35. package/dist/gs/embed/index.js +9 -3
  36. package/dist/gs/embed/index.js.map +1 -1
  37. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +1 -0
  38. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +33 -0
  39. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  40. package/dist/gs/github.com/mr-tron/base58/base58/index.js +4 -1
  41. package/dist/gs/github.com/mr-tron/base58/base58/index.js.map +1 -1
  42. package/dist/gs/golang.org/x/crypto/scrypt/index.d.ts +2 -0
  43. package/dist/gs/golang.org/x/crypto/scrypt/index.js +39 -0
  44. package/dist/gs/golang.org/x/crypto/scrypt/index.js.map +1 -0
  45. package/dist/gs/hash/fnv/index.js +13 -5
  46. package/dist/gs/hash/fnv/index.js.map +1 -1
  47. package/dist/gs/io/fs/glob.d.ts +3 -3
  48. package/dist/gs/io/fs/glob.js +9 -9
  49. package/dist/gs/io/fs/glob.js.map +1 -1
  50. package/dist/gs/io/fs/readdir.d.ts +2 -2
  51. package/dist/gs/io/fs/readdir.js +13 -74
  52. package/dist/gs/io/fs/readdir.js.map +1 -1
  53. package/dist/gs/io/fs/readlink.d.ts +1 -1
  54. package/dist/gs/io/fs/readlink.js +2 -2
  55. package/dist/gs/io/fs/readlink.js.map +1 -1
  56. package/dist/gs/io/fs/stat.d.ts +4 -2
  57. package/dist/gs/io/fs/stat.js +12 -73
  58. package/dist/gs/io/fs/stat.js.map +1 -1
  59. package/dist/gs/io/fs/sub.d.ts +2 -2
  60. package/dist/gs/io/fs/sub.js +11 -11
  61. package/dist/gs/io/fs/sub.js.map +1 -1
  62. package/dist/gs/io/fs/walk.js +2 -2
  63. package/dist/gs/io/fs/walk.js.map +1 -1
  64. package/dist/gs/maps/iter.js.map +1 -1
  65. package/dist/gs/maps/maps.js.map +1 -1
  66. package/dist/gs/mime/index.js +5 -2
  67. package/dist/gs/mime/index.js.map +1 -1
  68. package/dist/gs/net/http/httptest/index.js +6 -3
  69. package/dist/gs/net/http/httptest/index.js.map +1 -1
  70. package/dist/gs/net/http/index.d.ts +34 -18
  71. package/dist/gs/net/http/index.js +280 -63
  72. package/dist/gs/net/http/index.js.map +1 -1
  73. package/dist/gs/net/http/pprof/index.d.ts +5 -5
  74. package/dist/gs/net/http/pprof/index.js +21 -21
  75. package/dist/gs/net/http/pprof/index.js.map +1 -1
  76. package/dist/gs/reflect/iter.js +1 -1
  77. package/dist/gs/reflect/iter.js.map +1 -1
  78. package/dist/gs/reflect/type.d.ts +2 -0
  79. package/dist/gs/reflect/type.js +53 -21
  80. package/dist/gs/reflect/type.js.map +1 -1
  81. package/dist/gs/runtime/pprof/index.js.map +1 -1
  82. package/dist/gs/runtime/runtime.js +2 -2
  83. package/dist/gs/runtime/runtime.js.map +1 -1
  84. package/dist/gs/runtime/trace/index.js.map +1 -1
  85. package/dist/gs/slices/slices.d.ts +1 -1
  86. package/dist/gs/slices/slices.js +37 -4
  87. package/dist/gs/slices/slices.js.map +1 -1
  88. package/gs/builtin/builtin.ts +11 -14
  89. package/gs/builtin/defer.ts +2 -2
  90. package/gs/builtin/hostio.ts +5 -5
  91. package/gs/builtin/map.ts +4 -1
  92. package/gs/builtin/runtime-contract.test.ts +25 -0
  93. package/gs/builtin/slice.test.ts +14 -0
  94. package/gs/builtin/slice.ts +64 -0
  95. package/gs/builtin/type.ts +72 -0
  96. package/gs/bytes/bytes.test.ts +14 -13
  97. package/gs/compress/gzip/index.test.ts +86 -0
  98. package/gs/compress/gzip/index.ts +297 -0
  99. package/gs/compress/gzip/meta.json +6 -0
  100. package/gs/compress/gzip/parity.json +45 -0
  101. package/gs/compress/zlib/index.test.ts +19 -5
  102. package/gs/compress/zlib/index.ts +16 -7
  103. package/gs/context/context.test.ts +3 -1
  104. package/gs/crypto/ecdh/index.test.ts +6 -2
  105. package/gs/crypto/ecdh/index.ts +49 -12
  106. package/gs/crypto/ed25519/index.ts +20 -7
  107. package/gs/crypto/rand/index.ts +6 -3
  108. package/gs/embed/index.test.ts +4 -4
  109. package/gs/embed/index.ts +9 -3
  110. package/gs/fmt/fmt.test.ts +29 -4
  111. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +126 -0
  112. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +46 -0
  113. package/gs/github.com/mr-tron/base58/base58/index.ts +9 -3
  114. package/gs/github.com/zeebo/blake3/internal/consts/index.test.ts +2 -8
  115. package/gs/golang.org/x/crypto/scrypt/index.test.ts +81 -0
  116. package/gs/golang.org/x/crypto/scrypt/index.ts +54 -0
  117. package/gs/golang.org/x/crypto/scrypt/meta.json +5 -0
  118. package/gs/hash/fnv/index.test.ts +1 -8
  119. package/gs/hash/fnv/index.ts +27 -10
  120. package/gs/io/fs/glob.ts +14 -11
  121. package/gs/io/fs/meta.json +5 -0
  122. package/gs/io/fs/readdir.test.ts +63 -2
  123. package/gs/io/fs/readdir.ts +33 -30
  124. package/gs/io/fs/readlink.test.ts +2 -2
  125. package/gs/io/fs/readlink.ts +5 -2
  126. package/gs/io/fs/stat.test.ts +79 -0
  127. package/gs/io/fs/stat.ts +24 -10
  128. package/gs/io/fs/sub.test.ts +93 -0
  129. package/gs/io/fs/sub.ts +13 -13
  130. package/gs/io/fs/walk.ts +2 -2
  131. package/gs/maps/iter.ts +9 -9
  132. package/gs/maps/maps.ts +4 -4
  133. package/gs/math/bits/index.test.ts +10 -1
  134. package/gs/mime/index.test.ts +33 -15
  135. package/gs/mime/index.ts +9 -2
  136. package/gs/net/http/httptest/index.test.ts +17 -3
  137. package/gs/net/http/httptest/index.ts +8 -3
  138. package/gs/net/http/index.test.ts +851 -124
  139. package/gs/net/http/index.ts +612 -146
  140. package/gs/net/http/meta.json +3 -1
  141. package/gs/net/http/pprof/index.test.ts +4 -4
  142. package/gs/net/http/pprof/index.ts +43 -22
  143. package/gs/os/file_unix_js.test.ts +22 -0
  144. package/gs/reflect/iter.ts +4 -2
  145. package/gs/reflect/map.test.ts +56 -1
  146. package/gs/reflect/type.ts +76 -37
  147. package/gs/runtime/pprof/index.test.ts +7 -1
  148. package/gs/runtime/pprof/index.ts +5 -1
  149. package/gs/runtime/runtime.test.ts +7 -0
  150. package/gs/runtime/runtime.ts +2 -4
  151. package/gs/runtime/trace/index.test.ts +9 -1
  152. package/gs/runtime/trace/index.ts +5 -1
  153. package/gs/slices/meta.json +3 -0
  154. package/gs/slices/slices.test.ts +59 -21
  155. package/gs/slices/slices.ts +61 -20
  156. package/gs/strconv/complex.test.ts +17 -3
  157. package/gs/sync/atomic/doc_64.test.ts +2 -9
  158. package/gs/sync/sync.test.ts +18 -8
  159. package/gs/syscall/js/index.test.ts +9 -4
  160. package/package.json +5 -4
@@ -20,6 +20,7 @@ import {
20
20
  ErrServerClosed,
21
21
  File,
22
22
  FileServer,
23
+ FileServerFS,
23
24
  FileSystem,
24
25
  FS,
25
26
  Get,
@@ -124,12 +125,16 @@ describe('net/http override', () => {
124
125
  expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
125
126
  expect(StatusText(StatusForbidden)).toBe('Forbidden')
126
127
  expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
127
- expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
128
+ expect(StatusText(StatusUnsupportedMediaType)).toBe(
129
+ 'Unsupported Media Type',
130
+ )
128
131
  expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
129
132
  expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
130
133
  expect(StatusText(StatusBadGateway)).toBe('Bad Gateway')
131
134
  expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
132
- expect(StatusText(StatusNetworkAuthenticationRequired)).toBe('Network Authentication Required')
135
+ expect(StatusText(StatusNetworkAuthenticationRequired)).toBe(
136
+ 'Network Authentication Required',
137
+ )
133
138
  expect(StatusText(599)).toBe('')
134
139
  Header_Set(resp.Header, 'X-Test', 'ok')
135
140
  resp.Body = io.NopCloser(bytes.NewReader($.stringToBytes('body')))
@@ -164,12 +169,18 @@ describe('net/http override', () => {
164
169
  'text/plain',
165
170
  'charset=utf-8',
166
171
  ])
167
- expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain('copy')
168
- expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).not.toContain('copy')
172
+ expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain(
173
+ 'copy',
174
+ )
175
+ expect(
176
+ Array.from(Header_Values(header, 'Content-Type') ?? []),
177
+ ).not.toContain('copy')
169
178
 
170
179
  const written = new bytes.Buffer()
171
180
  expect(Header_Write(header, written)).toBeNull()
172
- expect(Buffer.from(written.Bytes()).toString('utf8')).toContain('Content-Type: text/plain\r\n')
181
+ expect(Buffer.from(written.Bytes()).toString('utf8')).toContain(
182
+ 'Content-Type: text/plain\r\n',
183
+ )
173
184
 
174
185
  expect(ParseHTTPVersion('HTTP/2.0')).toEqual([2, 0, true])
175
186
  expect(ParseHTTPVersion('h2')).toEqual([0, 0, false])
@@ -182,6 +193,36 @@ describe('net/http override', () => {
182
193
  expect(protocols.String()).toBe('{HTTP1,UnencryptedHTTP2}')
183
194
  })
184
195
 
196
+ it('accepts pointer-wrapped headers from generated ResponseWriter methods', () => {
197
+ const header = varRef(new Header())
198
+
199
+ Header_Set(header, 'content-length', '12')
200
+ Header_Add(header, 'content-type', 'text/plain')
201
+
202
+ expect(Header_Get(header, 'Content-Length')).toBe('12')
203
+ expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).toEqual([
204
+ 'text/plain',
205
+ ])
206
+
207
+ const cloned = Header_Clone(header)
208
+ Header_Del(header, 'Content-Type')
209
+
210
+ expect(Header_Get(header, 'Content-Type')).toBe('')
211
+ expect(Header_Get(cloned, 'Content-Type')).toBe('text/plain')
212
+ })
213
+
214
+ it('accepts interface-boxed named headers from generated interface calls', () => {
215
+ const header = $.namedValueInterfaceValue(new Header(), 'http.Header', {})
216
+
217
+ Header_Set(header, 'content-length', '12')
218
+ Header_Add(header, 'content-type', 'text/plain')
219
+
220
+ expect(Header_Get(header, 'Content-Length')).toBe('12')
221
+ expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).toEqual([
222
+ 'text/plain',
223
+ ])
224
+ })
225
+
185
226
  it('validates outgoing request construction', () => {
186
227
  const [req, reqErr] = NewRequestWithContext(
187
228
  context.Background(),
@@ -195,22 +236,58 @@ describe('net/http override', () => {
195
236
  expect(req?.URL?.Path).toBe('/path')
196
237
  expect(req?.URL?.RawQuery).toBe('q=1')
197
238
 
198
- expect(NewRequestWithContext(context.Background(), 'bad method', 'https://example.invalid/', null)[1]?.Error()).toBe(
199
- 'net/http: invalid method "bad method"',
200
- )
201
- expect(NewRequestWithContext(null, MethodGet, 'https://example.invalid/', null)[1]?.Error()).toBe(
202
- 'net/http: nil Context',
239
+ expect(
240
+ NewRequestWithContext(
241
+ context.Background(),
242
+ 'bad method',
243
+ 'https://example.invalid/',
244
+ null,
245
+ )[1]?.Error(),
246
+ ).toBe('net/http: invalid method "bad method"')
247
+ expect(
248
+ NewRequestWithContext(
249
+ null,
250
+ MethodGet,
251
+ 'https://example.invalid/',
252
+ null,
253
+ )[1]?.Error(),
254
+ ).toBe('net/http: nil Context')
255
+ expect(
256
+ NewRequestWithContext(
257
+ context.Background(),
258
+ MethodGet,
259
+ 'http://[::1',
260
+ null,
261
+ )[1],
262
+ ).not.toBeNull()
263
+ expect(
264
+ NewRequestWithContext(
265
+ context.Background(),
266
+ MethodGet,
267
+ 'http://x/%zz',
268
+ null,
269
+ )[1],
270
+ ).not.toBeNull()
271
+
272
+ const [bodyReq, bodyReqErr] = NewRequest(
273
+ MethodPost,
274
+ 'https://example.invalid/upload',
275
+ bytes.NewReader($.stringToBytes('abc')),
203
276
  )
204
- expect(NewRequestWithContext(context.Background(), MethodGet, 'http://[::1', null)[1]).not.toBeNull()
205
- expect(NewRequestWithContext(context.Background(), MethodGet, 'http://x/%zz', null)[1]).not.toBeNull()
206
-
207
- const [bodyReq, bodyReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', bytes.NewReader($.stringToBytes('abc')))
208
277
  expect(bodyReqErr).toBeNull()
209
278
  expect(bodyReq?.ContentLength).toBe(3)
210
- const [stringReq, stringReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', strings.NewReader('abcd'))
279
+ const [stringReq, stringReqErr] = NewRequest(
280
+ MethodPost,
281
+ 'https://example.invalid/upload',
282
+ strings.NewReader('abcd'),
283
+ )
211
284
  expect(stringReqErr).toBeNull()
212
285
  expect(stringReq?.ContentLength).toBe(4)
213
- const [noBodyReq, noBodyErr] = NewRequest(MethodPost, 'https://example.invalid/upload', NoBody)
286
+ const [noBodyReq, noBodyErr] = NewRequest(
287
+ MethodPost,
288
+ 'https://example.invalid/upload',
289
+ NoBody,
290
+ )
214
291
  expect(noBodyErr).toBeNull()
215
292
  expect(noBodyReq?.ContentLength).toBe(0)
216
293
  expect(noBodyReq?.Body).toBe(NoBody)
@@ -218,44 +295,84 @@ describe('net/http override', () => {
218
295
 
219
296
  it('applies cross-origin protection checks', () => {
220
297
  const protection = NewCrossOriginProtection()
221
- const [sameOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
298
+ const [sameOrigin] = NewRequest(
299
+ MethodPost,
300
+ 'https://example.invalid/update',
301
+ null,
302
+ )
222
303
  Header_Set(sameOrigin!.Header, 'Sec-Fetch-Site', 'same-origin')
223
304
  expect(protection.Check(sameOrigin)).toBeNull()
224
305
 
225
- const [noHeaders] = NewRequest(MethodPost, 'https://example.invalid/update', null)
306
+ const [noHeaders] = NewRequest(
307
+ MethodPost,
308
+ 'https://example.invalid/update',
309
+ null,
310
+ )
226
311
  expect(protection.Check(noHeaders)).toBeNull()
227
312
 
228
- const [safe] = NewRequest(MethodOptions, 'https://example.invalid/update', null)
313
+ const [safe] = NewRequest(
314
+ MethodOptions,
315
+ 'https://example.invalid/update',
316
+ null,
317
+ )
229
318
  Header_Set(safe!.Header, 'Sec-Fetch-Site', 'cross-site')
230
319
  expect(protection.Check(safe)).toBeNull()
231
320
 
232
- const [matchingOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
321
+ const [matchingOrigin] = NewRequest(
322
+ MethodPost,
323
+ 'https://example.invalid/update',
324
+ null,
325
+ )
233
326
  Header_Set(matchingOrigin!.Header, 'Origin', 'https://example.invalid')
234
327
  expect(protection.Check(matchingOrigin)).toBeNull()
235
328
 
236
- const [crossSite] = NewRequest(MethodPost, 'https://example.invalid/update', null)
329
+ const [crossSite] = NewRequest(
330
+ MethodPost,
331
+ 'https://example.invalid/update',
332
+ null,
333
+ )
237
334
  Header_Set(crossSite!.Header, 'Sec-Fetch-Site', 'cross-site')
238
335
  expect(protection.Check(crossSite)?.Error()).toContain('Sec-Fetch-Site')
239
336
 
240
- const [oldBrowser] = NewRequest(MethodPost, 'https://example.invalid/update', null)
337
+ const [oldBrowser] = NewRequest(
338
+ MethodPost,
339
+ 'https://example.invalid/update',
340
+ null,
341
+ )
241
342
  Header_Set(oldBrowser!.Header, 'Origin', 'https://attacker.invalid')
242
- expect(protection.Check(oldBrowser)?.Error()).toContain('Origin does not match Host')
343
+ expect(protection.Check(oldBrowser)?.Error()).toContain(
344
+ 'Origin does not match Host',
345
+ )
243
346
 
244
347
  expect(protection.AddTrustedOrigin('https://trusted.invalid')).toBeNull()
245
- expect(protection.AddTrustedOrigin('https://trusted.invalid/')).not.toBeNull()
246
- const [trusted] = NewRequest(MethodPost, 'https://example.invalid/update', null)
348
+ expect(
349
+ protection.AddTrustedOrigin('https://trusted.invalid/'),
350
+ ).not.toBeNull()
351
+ const [trusted] = NewRequest(
352
+ MethodPost,
353
+ 'https://example.invalid/update',
354
+ null,
355
+ )
247
356
  Header_Set(trusted!.Header, 'Origin', 'https://trusted.invalid')
248
357
  Header_Set(trusted!.Header, 'Sec-Fetch-Site', 'cross-site')
249
358
  expect(protection.Check(trusted)).toBeNull()
250
359
 
251
360
  protection.AddInsecureBypassPattern('/bypass/')
252
361
  protection.AddInsecureBypassPattern('POST /post-only/')
253
- const [bypass] = NewRequest(MethodPost, 'https://example.invalid/bypass/ok', null)
362
+ const [bypass] = NewRequest(
363
+ MethodPost,
364
+ 'https://example.invalid/bypass/ok',
365
+ null,
366
+ )
254
367
  Header_Set(bypass!.Header, 'Origin', 'https://attacker.invalid')
255
368
  Header_Set(bypass!.Header, 'Sec-Fetch-Site', 'cross-site')
256
369
  expect(protection.Check(bypass)).toBeNull()
257
370
 
258
- const [methodBypass] = NewRequest(MethodPost, 'https://example.invalid/post-only/', null)
371
+ const [methodBypass] = NewRequest(
372
+ MethodPost,
373
+ 'https://example.invalid/post-only/',
374
+ null,
375
+ )
259
376
  Header_Set(methodBypass!.Header, 'Origin', 'https://attacker.invalid')
260
377
  expect(protection.Check(methodBypass)).toBeNull()
261
378
  })
@@ -269,7 +386,11 @@ describe('net/http override', () => {
269
386
  w?.WriteHeader(StatusOK)
270
387
  },
271
388
  })
272
- const [blocked] = NewRequest(MethodPost, 'https://example.invalid/update', null)
389
+ const [blocked] = NewRequest(
390
+ MethodPost,
391
+ 'https://example.invalid/update',
392
+ null,
393
+ )
273
394
  Header_Set(blocked!.Header, 'Sec-Fetch-Site', 'cross-site')
274
395
  const blockedWriter = new testResponseWriter()
275
396
 
@@ -304,7 +425,9 @@ describe('net/http override', () => {
304
425
  expect(cookies?.[1]?.Value).toBe('v2')
305
426
 
306
427
  expect(ParseCookie('')[1]?.Error()).toBe('http: blank cookie')
307
- expect(ParseCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
428
+ expect(ParseCookie('missing-equals')[1]?.Error()).toBe(
429
+ "http: '=' not found in cookie",
430
+ )
308
431
  expect(ParseCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
309
432
  expect(ParseCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
310
433
 
@@ -330,9 +453,13 @@ describe('net/http override', () => {
330
453
  expect(spaced?.Quoted).toBe(true)
331
454
 
332
455
  expect(ParseSetCookie('')[1]?.Error()).toBe('http: blank cookie')
333
- expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
456
+ expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe(
457
+ "http: '=' not found in cookie",
458
+ )
334
459
  expect(ParseSetCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
335
- expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
460
+ expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe(
461
+ 'http: invalid cookie value',
462
+ )
336
463
  })
337
464
 
338
465
  it('exports no-body, limit-reader, and unsupported controller surfaces', async () => {
@@ -342,34 +469,48 @@ describe('net/http override', () => {
342
469
  expect(err).toBe(io.EOF)
343
470
  expect(NoBody.Close()).toBeNull()
344
471
 
345
- const limited = MaxBytesReader(null, {
346
- Read: (p: Uint8Array) => {
347
- p[0] = 1
348
- if (p.length > 1) {
349
- p[1] = 2
350
- }
351
- return [1, null]
472
+ const limited = MaxBytesReader(
473
+ null,
474
+ {
475
+ Read: (p: Uint8Array) => {
476
+ p[0] = 1
477
+ if (p.length > 1) {
478
+ p[1] = 2
479
+ }
480
+ return [1, null]
481
+ },
482
+ Close: () => null,
352
483
  },
353
- Close: () => null,
354
- }, 1)
484
+ 1,
485
+ )
355
486
  const limitedBuf = new Uint8Array(2)
356
487
  expect(limited.Read(limitedBuf)).toEqual([1, null])
357
488
  expect(limitedBuf[0]).toBe(1)
358
489
  const [, limitErr] = limited.Read(new Uint8Array(1))
359
490
  expect(limitErr).toBeInstanceOf(MaxBytesError)
360
491
 
361
- const exactReader = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('ok'))), 2)
492
+ const exactReader = MaxBytesReader(
493
+ null,
494
+ io.NopCloser(bytes.NewReader($.stringToBytes('ok'))),
495
+ 2,
496
+ )
362
497
  const [exactData, exactErr] = await io.ReadAll(exactReader)
363
498
  expect(exactErr).toBeNull()
364
499
  expect(Buffer.from(exactData ?? []).toString('utf8')).toBe('ok')
365
500
 
366
- const tooLarge = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))), 2)
501
+ const tooLarge = MaxBytesReader(
502
+ null,
503
+ io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))),
504
+ 2,
505
+ )
367
506
  const tooLargeBuf = new Uint8Array(8)
368
507
  const [tooLargeN, tooLargeErr] = tooLarge.Read(tooLargeBuf)
369
508
  expect(tooLargeN).toBe(2)
370
509
  expect(tooLargeErr).toBeInstanceOf(MaxBytesError)
371
510
  expect((tooLargeErr as MaxBytesError).Limit).toBe(2)
372
- expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe('to')
511
+ expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe(
512
+ 'to',
513
+ )
373
514
 
374
515
  const controller = NewResponseController(null)
375
516
  expect(controller.Hijack()[2]).toBe(ErrNotSupported)
@@ -382,12 +523,15 @@ describe('net/http override', () => {
382
523
  const body = io.NopCloser(bytes.NewReader($.stringToBytes('ok')))
383
524
  const [req] = NewRequest(MethodPost, 'http://example.invalid/upload', body)
384
525
  let servedReq: any = null
385
- const handler = MaxBytesHandler({
386
- ServeHTTP(_w, r) {
387
- servedReq = $.pointerValue(r)
388
- Header_Set(servedReq.Header, 'X-Shared', 'true')
526
+ const handler = MaxBytesHandler(
527
+ {
528
+ ServeHTTP(_w, r) {
529
+ servedReq = $.pointerValue(r)
530
+ Header_Set(servedReq.Header, 'X-Shared', 'true')
531
+ },
389
532
  },
390
- }, 1)
533
+ 1,
534
+ )
391
535
 
392
536
  handler.ServeHTTP(null, req)
393
537
 
@@ -424,7 +568,11 @@ describe('net/http override', () => {
424
568
  throw new Error('network down')
425
569
  },
426
570
  })
427
- const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid', null)
571
+ const [req, reqErr] = NewRequest(
572
+ MethodPost,
573
+ 'https://example.invalid',
574
+ null,
575
+ )
428
576
  expect(reqErr).toBeNull()
429
577
  expect((req!.URL as any).Path).toBe('/')
430
578
  expect(req!.Host).toBe('example.invalid')
@@ -440,13 +588,21 @@ describe('net/http override', () => {
440
588
  Read: (p: Uint8Array) => [p.length, null] as [number, null],
441
589
  }
442
590
 
443
- const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', reader)
591
+ const [req, reqErr] = NewRequest(
592
+ MethodPost,
593
+ 'https://example.invalid/upload',
594
+ reader,
595
+ )
444
596
 
445
597
  expect(reqErr).toBeNull()
446
598
  expect(req!.Body).not.toBeNull()
447
599
  expect(req!.Body!.Close()).toBeNull()
448
600
 
449
- const resp = new Response({ StatusCode: StatusCreated, ContentLength: -1, Request: varRef(req!) })
601
+ const resp = new Response({
602
+ StatusCode: StatusCreated,
603
+ ContentLength: -1,
604
+ Request: varRef(req!),
605
+ })
450
606
  expect(resp.ContentLength).toBe(-1)
451
607
  expect((resp.Request as any).value).toBe(req)
452
608
  })
@@ -462,7 +618,9 @@ describe('net/http override', () => {
462
618
  const headers = init?.headers as Headers
463
619
  expect(headers.get('Range')).toBe('bytes=0-9')
464
620
  expect(headers.get('Authorization')).toBe('Bearer test')
465
- expect(Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8')).toBe('payload')
621
+ expect(
622
+ Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8'),
623
+ ).toBe('payload')
466
624
  return new globalThis.Response('accepted', {
467
625
  status: StatusCreated,
468
626
  statusText: 'Created',
@@ -478,7 +636,11 @@ describe('net/http override', () => {
478
636
  return null
479
637
  },
480
638
  }
481
- const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', requestBody)
639
+ const [req] = NewRequest(
640
+ MethodPost,
641
+ 'https://example.invalid/upload',
642
+ requestBody,
643
+ )
482
644
  Header_Set(req!.Header, 'Range', 'bytes=0-9')
483
645
  Header_Set(req!.Header, 'Authorization', 'Bearer test')
484
646
 
@@ -491,6 +653,135 @@ describe('net/http override', () => {
491
653
  expect(requestBodyClosed).toBe(true)
492
654
  })
493
655
 
656
+ it('returns from fetch-backed RoundTrip after response headers', async () => {
657
+ let arrayBufferCalled = false
658
+ Object.defineProperty(globalThis, 'fetch', {
659
+ configurable: true,
660
+ writable: true,
661
+ value: async () => {
662
+ const response = new globalThis.Response(null, {
663
+ status: StatusOK,
664
+ statusText: 'OK',
665
+ headers: { 'Content-Length': '5' },
666
+ })
667
+ Object.defineProperty(response, 'arrayBuffer', {
668
+ configurable: true,
669
+ value: () => {
670
+ arrayBufferCalled = true
671
+ return new Promise<ArrayBuffer>(() => {})
672
+ },
673
+ })
674
+ return response
675
+ },
676
+ })
677
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
678
+
679
+ const [resp, err] = await Promise.race([
680
+ DefaultTransport.RoundTrip(req),
681
+ new Promise<never>((_, reject) =>
682
+ setTimeout(() => reject(new Error('RoundTrip did not return')), 25),
683
+ ),
684
+ ])
685
+
686
+ expect(err).toBeNull()
687
+ expect(resp?.StatusCode).toBe(StatusOK)
688
+ expect(resp?.ContentLength).toBe(5)
689
+ expect(arrayBufferCalled).toBe(false)
690
+ expect(resp?.Body?.Close()).toBeNull()
691
+ })
692
+
693
+ it('reads fetch-backed response bodies after RoundTrip returns', async () => {
694
+ Object.defineProperty(globalThis, 'fetch', {
695
+ configurable: true,
696
+ writable: true,
697
+ value: async () =>
698
+ new globalThis.Response('accepted', {
699
+ status: StatusCreated,
700
+ statusText: 'Created',
701
+ headers: { 'Content-Length': '8' },
702
+ }),
703
+ })
704
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
705
+
706
+ const [resp, err] = await DefaultTransport.RoundTrip(req)
707
+ const buf = new Uint8Array(16)
708
+ const [n, readErr] = await (resp!.Body!.Read(buf) as any)
709
+
710
+ expect(err).toBeNull()
711
+ expect(readErr).toBeNull()
712
+ expect(n).toBe(8)
713
+ expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('accepted')
714
+ expect(resp?.Body?.Close()).toBeNull()
715
+ })
716
+
717
+ it('aborts pending fetches when the request context is canceled', async () => {
718
+ let fetchSignal: AbortSignal | undefined
719
+ let fetchStarted: (() => void) | null = null
720
+ const fetchStartedPromise = new Promise<void>((resolve) => {
721
+ fetchStarted = resolve
722
+ })
723
+ Object.defineProperty(globalThis, 'fetch', {
724
+ configurable: true,
725
+ writable: true,
726
+ value: async (_input: RequestInfo | URL, init?: RequestInit) => {
727
+ fetchSignal = init?.signal ?? undefined
728
+ fetchStarted?.()
729
+ return new Promise<globalThis.Response>((_resolve, reject) => {
730
+ fetchSignal?.addEventListener('abort', () => {
731
+ reject(new Error('aborted'))
732
+ })
733
+ })
734
+ },
735
+ })
736
+ const [ctx, cancel] = context.WithCancel(context.Background())
737
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
738
+
739
+ const roundTrip = DefaultTransport.RoundTrip(req!.WithContext(ctx))
740
+ await fetchStartedPromise
741
+ cancel?.()
742
+ const [resp, err] = await roundTrip
743
+
744
+ expect(resp).toBeNull()
745
+ expect(err).toBe(context.Canceled)
746
+ expect(fetchSignal?.aborted).toBe(true)
747
+ })
748
+
749
+ it('aborts pending fetch body reads when the request context is canceled', async () => {
750
+ let fetchSignal: AbortSignal | undefined
751
+ Object.defineProperty(globalThis, 'fetch', {
752
+ configurable: true,
753
+ writable: true,
754
+ value: async (_input: RequestInfo | URL, init?: RequestInit) => {
755
+ fetchSignal = init?.signal ?? undefined
756
+ const response = new globalThis.Response(null, { status: StatusOK })
757
+ Object.defineProperty(response, 'arrayBuffer', {
758
+ configurable: true,
759
+ value: () =>
760
+ new Promise<ArrayBuffer>((_resolve, reject) => {
761
+ fetchSignal?.addEventListener('abort', () => {
762
+ reject(new Error('body aborted'))
763
+ })
764
+ }),
765
+ })
766
+ return response
767
+ },
768
+ })
769
+ const [ctx, cancel] = context.WithCancel(context.Background())
770
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
771
+
772
+ const [resp, roundTripErr] = await DefaultTransport.RoundTrip(
773
+ req!.WithContext(ctx),
774
+ )
775
+ const read = resp!.Body!.Read(new Uint8Array(8)) as any
776
+ cancel?.()
777
+ const [n, readErr] = await read
778
+
779
+ expect(roundTripErr).toBeNull()
780
+ expect(n).toBe(0)
781
+ expect(readErr).toBe(context.Canceled)
782
+ expect(fetchSignal?.aborted).toBe(true)
783
+ })
784
+
494
785
  it('closes request bodies when fetch body reads fail', async () => {
495
786
  let requestBodyClosed = false
496
787
  Object.defineProperty(globalThis, 'fetch', {
@@ -523,15 +814,20 @@ describe('net/http override', () => {
523
814
  writable: true,
524
815
  value: undefined,
525
816
  })
526
- const [unsupportedReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
527
- Read: () => [0, null],
528
- Close: () => {
529
- unsupportedClosed = true
530
- return null
817
+ const [unsupportedReq] = NewRequest(
818
+ MethodPost,
819
+ 'https://example.invalid/upload',
820
+ {
821
+ Read: () => [0, null],
822
+ Close: () => {
823
+ unsupportedClosed = true
824
+ return null
825
+ },
531
826
  },
532
- })
827
+ )
533
828
 
534
- const [unsupportedResp, unsupportedErr] = await DefaultTransport.RoundTrip(unsupportedReq)
829
+ const [unsupportedResp, unsupportedErr] =
830
+ await DefaultTransport.RoundTrip(unsupportedReq)
535
831
 
536
832
  expect(unsupportedResp).toBeNull()
537
833
  expect(unsupportedErr?.Error()).toContain('Client.Do is not implemented')
@@ -547,15 +843,21 @@ describe('net/http override', () => {
547
843
  })
548
844
  const [ctx, cancel] = context.WithCancel(context.Background())
549
845
  cancel?.()
550
- const [canceledReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
551
- Read: () => [0, null],
552
- Close: () => {
553
- canceledClosed = true
554
- return null
846
+ const [canceledReq] = NewRequest(
847
+ MethodPost,
848
+ 'https://example.invalid/upload',
849
+ {
850
+ Read: () => [0, null],
851
+ Close: () => {
852
+ canceledClosed = true
853
+ return null
854
+ },
555
855
  },
556
- })
856
+ )
557
857
 
558
- const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(canceledReq!.WithContext(ctx))
858
+ const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(
859
+ canceledReq!.WithContext(ctx),
860
+ )
559
861
 
560
862
  expect(canceledResp).toBeNull()
561
863
  expect(canceledErr).toBe(context.Canceled)
@@ -670,10 +972,14 @@ describe('net/http override', () => {
670
972
 
671
973
  it('posts URL-encoded forms through clients and package helper', async () => {
672
974
  const transport = {
673
- async RoundTrip(got: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
975
+ async RoundTrip(
976
+ got: Request | $.VarRef<Request> | null,
977
+ ): Promise<[Response | null, $.GoError]> {
674
978
  const request = $.pointerValue<Request>(got)
675
979
  expect(request.Method).toBe(MethodPost)
676
- expect(Header_Get(request.Header, 'Content-Type')).toBe('application/x-www-form-urlencoded')
980
+ expect(Header_Get(request.Header, 'Content-Type')).toBe(
981
+ 'application/x-www-form-urlencoded',
982
+ )
677
983
  const [data, err] = await io.ReadAll(request.Body!)
678
984
  expect(err).toBeNull()
679
985
  expect($.bytesToString(data)).toBe('a=one&a=two&space=x+y')
@@ -686,7 +992,10 @@ describe('net/http override', () => {
686
992
  ])
687
993
  const client = new Client({ Transport: transport })
688
994
 
689
- const [clientResp, clientErr] = await client.PostForm('https://example.invalid/form', form)
995
+ const [clientResp, clientErr] = await client.PostForm(
996
+ 'https://example.invalid/form',
997
+ form,
998
+ )
690
999
  expect(clientErr).toBeNull()
691
1000
  expect(clientResp?.StatusCode).toBe(StatusOK)
692
1001
 
@@ -712,7 +1021,10 @@ describe('net/http override', () => {
712
1021
 
713
1022
  Header_Add(header, 'x-pack-id', 'pack-1')
714
1023
  Header_Add(header, 'X-Pack-Id', 'pack-2')
715
- expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual(['pack-1', 'pack-2'])
1024
+ expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual([
1025
+ 'pack-1',
1026
+ 'pack-2',
1027
+ ])
716
1028
 
717
1029
  Header_Del(header, 'x-pack-id')
718
1030
  expect(Header_Get(header, 'X-Pack-ID')).toBe('')
@@ -721,7 +1033,7 @@ describe('net/http override', () => {
721
1033
  it('accepts server context and shutdown surfaces', () => {
722
1034
  const srv = new Server({
723
1035
  Addr: ':0',
724
- BaseContext: () => ({} as any),
1036
+ BaseContext: () => ({}) as any,
725
1037
  ReadHeaderTimeout: 10,
726
1038
  })
727
1039
 
@@ -729,7 +1041,9 @@ describe('net/http override', () => {
729
1041
  expect(srv.BaseContext?.(null)).toEqual({})
730
1042
  expect(srv.ReadHeaderTimeout).toBe(10)
731
1043
  expect(srv.Shutdown({} as any)).toBeNull()
732
- expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe('net/http: Server.ListenAndServeTLS is not implemented in GoScript')
1044
+ expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe(
1045
+ 'net/http: Server.ListenAndServeTLS is not implemented in GoScript',
1046
+ )
733
1047
  })
734
1048
 
735
1049
  it('supports handler functions and not-found responses for typechecked server tests', () => {
@@ -749,6 +1063,116 @@ describe('net/http override', () => {
749
1063
  expect(writes).toEqual(['status:404', '404 page not found\n'])
750
1064
  })
751
1065
 
1066
+ it('accepts nullable handler function receivers for generated method calls', () => {
1067
+ const writer: ResponseWriter = {
1068
+ Header: () => new Header(),
1069
+ Write: (p) => [p?.length ?? 0, null],
1070
+ WriteHeader: () => undefined,
1071
+ }
1072
+
1073
+ expect(typeof HandlerFunc_ServeHTTP).toBe('function')
1074
+ expect(writer.Header()).toBeInstanceOf(Header)
1075
+ })
1076
+
1077
+ it('registers Handler for generic runtime assertions', () => {
1078
+ const handlerFunc = (_w: ResponseWriter | null, _r: Request | null) =>
1079
+ undefined
1080
+ const handler = $.namedValueInterfaceValue(
1081
+ $.namedFunction(
1082
+ $.functionValue(handlerFunc, {
1083
+ kind: $.TypeKind.Function,
1084
+ params: [
1085
+ 'http.ResponseWriter',
1086
+ {
1087
+ kind: $.TypeKind.Pointer,
1088
+ elemType: 'http.Request',
1089
+ },
1090
+ ],
1091
+ results: [],
1092
+ }),
1093
+ 'http.HandlerFunc',
1094
+ {
1095
+ kind: $.TypeKind.Function,
1096
+ name: 'http.HandlerFunc',
1097
+ params: [
1098
+ 'http.ResponseWriter',
1099
+ {
1100
+ kind: $.TypeKind.Pointer,
1101
+ elemType: 'http.Request',
1102
+ },
1103
+ ],
1104
+ results: [],
1105
+ },
1106
+ ),
1107
+ 'http.HandlerFunc',
1108
+ {
1109
+ ServeHTTP: (receiver, ...args) =>
1110
+ HandlerFunc_ServeHTTP(
1111
+ $.isVarRef(receiver) ? receiver.value : receiver,
1112
+ ...(args as [ResponseWriter | null, Request | null]),
1113
+ ),
1114
+ },
1115
+ {
1116
+ kind: $.TypeKind.Function,
1117
+ name: 'http.HandlerFunc',
1118
+ params: [
1119
+ 'http.ResponseWriter',
1120
+ {
1121
+ kind: $.TypeKind.Pointer,
1122
+ elemType: 'http.Request',
1123
+ },
1124
+ ],
1125
+ results: [],
1126
+ },
1127
+ )
1128
+
1129
+ const [asserted, ok] = $.typeAssertTuple<{
1130
+ ServeHTTP(w: ResponseWriter | null, r: Request | null): void
1131
+ }>(handler, 'http.Handler')
1132
+
1133
+ expect(ok).toBe(true)
1134
+ expect(asserted).toBe(handler)
1135
+ })
1136
+
1137
+ it('rejects named method wrappers with incompatible interface arity', () => {
1138
+ const handler = $.namedValueInterfaceValue(
1139
+ $.namedFunction(
1140
+ $.functionValue(() => undefined, {
1141
+ kind: $.TypeKind.Function,
1142
+ params: [],
1143
+ results: [],
1144
+ }),
1145
+ 'http.HandlerFunc',
1146
+ {
1147
+ kind: $.TypeKind.Function,
1148
+ name: 'http.HandlerFunc',
1149
+ params: [],
1150
+ results: [],
1151
+ },
1152
+ ),
1153
+ 'http.HandlerFunc',
1154
+ {
1155
+ ServeHTTP: (receiver, ...args) =>
1156
+ HandlerFunc_ServeHTTP(
1157
+ $.isVarRef(receiver) ? receiver.value : receiver,
1158
+ ...(args as [ResponseWriter | null, Request | null]),
1159
+ ),
1160
+ },
1161
+ {
1162
+ kind: $.TypeKind.Function,
1163
+ name: 'http.HandlerFunc',
1164
+ params: [],
1165
+ results: [],
1166
+ },
1167
+ )
1168
+
1169
+ const [, ok] = $.typeAssertTuple<{
1170
+ ServeHTTP(w: ResponseWriter | null, r: Request | null): void
1171
+ }>(handler, 'http.Handler')
1172
+
1173
+ expect(ok).toBe(false)
1174
+ })
1175
+
752
1176
  it('routes through default mux and handler helper exports', () => {
753
1177
  const writes: string[] = []
754
1178
  const writer: ResponseWriter = {
@@ -778,7 +1202,7 @@ describe('net/http override', () => {
778
1202
  ])
779
1203
  })
780
1204
 
781
- it('formats Set-Cookie headers for browser bootstrap routes', () => {
1205
+ it('formats Set-Cookie headers for browser bootstrap routes', async () => {
782
1206
  const header = new Header()
783
1207
  const writer: ResponseWriter = {
784
1208
  Header: () => header,
@@ -786,15 +1210,18 @@ describe('net/http override', () => {
786
1210
  WriteHeader: () => undefined,
787
1211
  }
788
1212
 
789
- SetCookie(writer, new Cookie({
790
- Name: 'spacewave_local_capability',
791
- Value: 'token',
792
- Path: '/',
793
- MaxAge: 300,
794
- HttpOnly: true,
795
- Secure: true,
796
- SameSite: SameSiteStrictMode,
797
- }))
1213
+ await SetCookie(
1214
+ writer,
1215
+ new Cookie({
1216
+ Name: 'spacewave_local_capability',
1217
+ Value: 'token',
1218
+ Path: '/',
1219
+ MaxAge: 300,
1220
+ HttpOnly: true,
1221
+ Secure: true,
1222
+ SameSite: SameSiteStrictMode,
1223
+ }),
1224
+ )
798
1225
 
799
1226
  expect(Array.from(header.get('Set-Cookie') ?? [])).toEqual([
800
1227
  'spacewave_local_capability=token; Path=/; Max-Age=300; HttpOnly; Secure; SameSite=Strict',
@@ -806,29 +1233,46 @@ describe('net/http override', () => {
806
1233
 
807
1234
  expect(err).toBeNull()
808
1235
  expect(parsed.Unix()).toBe(784111777)
809
- expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe('text/html; charset=utf-8')
1236
+ expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe(
1237
+ 'text/html; charset=utf-8',
1238
+ )
810
1239
  expect(
811
- DetectContentType(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
1240
+ DetectContentType(
1241
+ new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
1242
+ ),
812
1243
  ).toBe('image/png')
813
- expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe('image/jpeg')
814
- expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe('application/pdf')
815
- expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe('image/webp')
816
- expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe('audio/aiff')
1244
+ expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe(
1245
+ 'image/jpeg',
1246
+ )
1247
+ expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe(
1248
+ 'application/pdf',
1249
+ )
1250
+ expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe(
1251
+ 'image/webp',
1252
+ )
1253
+ expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe(
1254
+ 'audio/aiff',
1255
+ )
817
1256
  expect(DetectContentType($.stringToBytes('ID3payload'))).toBe('audio/mpeg')
818
1257
  expect(DetectContentType($.stringToBytes('wOFFpayload'))).toBe('font/woff')
819
- expect(DetectContentType(new Uint8Array([
820
- 0x00, 0x00, 0x00, 0x18,
821
- 0x66, 0x74, 0x79, 0x70,
822
- 0x69, 0x73, 0x6f, 0x6d,
823
- 0x00, 0x00, 0x00, 0x00,
824
- 0x6d, 0x70, 0x34, 0x31,
825
- 0x00, 0x00, 0x00, 0x00,
826
- ]))).toBe('video/mp4')
827
- expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe('application/octet-stream')
828
- expect(DetectContentType(new Uint8Array())).toBe('text/plain; charset=utf-8')
829
- })
830
-
831
- it('exports file server interface shapes', () => {
1258
+ expect(
1259
+ DetectContentType(
1260
+ new Uint8Array([
1261
+ 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f,
1262
+ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x70, 0x34, 0x31, 0x00, 0x00,
1263
+ 0x00, 0x00,
1264
+ ]),
1265
+ ),
1266
+ ).toBe('video/mp4')
1267
+ expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe(
1268
+ 'application/octet-stream',
1269
+ )
1270
+ expect(DetectContentType(new Uint8Array())).toBe(
1271
+ 'text/plain; charset=utf-8',
1272
+ )
1273
+ })
1274
+
1275
+ it('exports file server interface shapes', async () => {
832
1276
  const file: File = {
833
1277
  Close: () => null,
834
1278
  Read: (p) => [p?.length ?? 0, null],
@@ -837,11 +1281,15 @@ describe('net/http override', () => {
837
1281
  Stat: () => [null, null],
838
1282
  }
839
1283
  const fsys: FileSystem = {
840
- Open: (name) => (name === 'ok' ? [file, null] : [null, new Error('missing')]),
1284
+ Open: (name) =>
1285
+ name === 'ok' ? [file, null] : [null, new Error('missing')],
841
1286
  }
842
1287
 
843
- expect(fsys.Open('ok')[0]).toBe(file)
844
- expect(fsys.Open('missing')[1]?.message).toBe('missing')
1288
+ const [okFile] = await fsys.Open('ok')
1289
+ const [, missingErr] = await fsys.Open('missing')
1290
+
1291
+ expect(okFile).toBe(file)
1292
+ expect(missingErr?.message).toBe('missing')
845
1293
  })
846
1294
 
847
1295
  it('serves files and omits HEAD response bodies', async () => {
@@ -853,23 +1301,26 @@ describe('net/http override', () => {
853
1301
  Read: (p: Uint8Array) => reader.Read(p),
854
1302
  Seek: (offset: number, whence: number) => reader.Seek(offset, whence),
855
1303
  Readdir: () => [null, null] as [null, null],
856
- Stat: () => [
857
- {
858
- IsDir: () => false,
859
- ModTime: () => null as never,
860
- Mode: () => 0,
861
- Name: () => 'file.txt',
862
- Size: () => 5,
863
- Sys: () => null,
864
- },
865
- null,
866
- ] as const,
1304
+ Stat: () =>
1305
+ [
1306
+ {
1307
+ IsDir: () => false,
1308
+ ModTime: () => null as never,
1309
+ Mode: () => 0,
1310
+ Name: () => 'file.txt',
1311
+ Size: () => 5,
1312
+ Sys: () => null,
1313
+ },
1314
+ null,
1315
+ ] as const,
867
1316
  }
868
1317
  }
869
1318
  const root = FS({
870
1319
  Open: (name) => {
871
1320
  opened.push(name)
872
- return name === 'file.txt' ? [makeFile(), null] : [null, new Error('missing')]
1321
+ return name === 'file.txt' ?
1322
+ [makeFile(), null]
1323
+ : [null, new Error('missing')]
873
1324
  },
874
1325
  })
875
1326
  const writes: string[] = []
@@ -883,17 +1334,26 @@ describe('net/http override', () => {
883
1334
  WriteHeader: (code) => writes.push(`status:${code}`),
884
1335
  }
885
1336
  const handler = FileServer(root)
886
- const [getReq] = NewRequest(MethodGet, 'http://example.invalid/../file.txt', null)
1337
+ const [getReq] = NewRequest(
1338
+ MethodGet,
1339
+ 'http://example.invalid/../file.txt',
1340
+ null,
1341
+ )
887
1342
 
888
1343
  await handler.ServeHTTP(writer, getReq)
889
1344
 
890
1345
  expect(opened).toEqual(['file.txt'])
891
1346
  expect(writes).toEqual(['status:200', 'hello'])
892
1347
  expect(Header_Get(header, 'Content-Length')).toBe('5')
1348
+ expect(Header_Get(header, 'Content-Type')).toBe('text/plain; charset=utf-8')
893
1349
 
894
1350
  writes.length = 0
895
1351
  opened.length = 0
896
- const [headReq] = NewRequest(MethodHead, 'http://example.invalid/file.txt', null)
1352
+ const [headReq] = NewRequest(
1353
+ MethodHead,
1354
+ 'http://example.invalid/file.txt',
1355
+ null,
1356
+ )
897
1357
 
898
1358
  await handler.ServeHTTP(writer, headReq)
899
1359
 
@@ -901,6 +1361,249 @@ describe('net/http override', () => {
901
1361
  expect(writes).toEqual(['status:200'])
902
1362
  })
903
1363
 
1364
+ it('serves files from async file systems and file methods', async () => {
1365
+ const closeCalls: string[] = []
1366
+ const root = {
1367
+ async Open(name: string) {
1368
+ if (name !== 'async.txt') {
1369
+ return [null, new Error('missing')]
1370
+ }
1371
+ let offset = 0
1372
+ const file = {
1373
+ async Close() {
1374
+ closeCalls.push(name)
1375
+ return null
1376
+ },
1377
+ async Read(p) {
1378
+ const data = $.stringToBytes('async-body')
1379
+ if (offset >= data.length) {
1380
+ return [0, io.EOF]
1381
+ }
1382
+ const n = Math.min(p.length, data.length - offset)
1383
+ p.set(data.subarray(offset, offset + n), 0)
1384
+ offset += n
1385
+ return [n, null]
1386
+ },
1387
+ async Seek(seekOffset) {
1388
+ offset = Number(seekOffset)
1389
+ return [offset, null]
1390
+ },
1391
+ async Readdir() {
1392
+ return [null, null]
1393
+ },
1394
+ async Stat() {
1395
+ return [
1396
+ {
1397
+ IsDir: () => false,
1398
+ ModTime: () => null as never,
1399
+ Mode: () => 0,
1400
+ Name: () => name,
1401
+ Size: () => 10,
1402
+ Sys: () => null,
1403
+ },
1404
+ null,
1405
+ ]
1406
+ },
1407
+ }
1408
+ return [file, null]
1409
+ },
1410
+ }
1411
+ const writes: string[] = []
1412
+ const header = new Header()
1413
+ const writer: ResponseWriter = {
1414
+ Header: () => header,
1415
+ Write: (p) => {
1416
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
1417
+ return [p?.length ?? 0, null]
1418
+ },
1419
+ WriteHeader: (code) => writes.push(`status:${code}`),
1420
+ }
1421
+ const [req] = NewRequest(
1422
+ MethodGet,
1423
+ 'http://example.invalid/async.txt',
1424
+ null,
1425
+ )
1426
+
1427
+ await FileServer(root).ServeHTTP(writer, req)
1428
+
1429
+ expect(writes).toEqual(['status:200', 'async-body'])
1430
+ expect(Header_Get(header, 'Content-Length')).toBe('10')
1431
+ expect(closeCalls).toEqual(['async.txt'])
1432
+ })
1433
+
1434
+ it('serves files through FS adapters backed by async io/fs Open', async () => {
1435
+ const opened: string[] = []
1436
+ const root = {
1437
+ async Open(name: string) {
1438
+ opened.push(name)
1439
+ const data = $.stringToBytes('fs-adapter')
1440
+ let offset = 0
1441
+ return [
1442
+ {
1443
+ Close: async () => null,
1444
+ Read: async (p: $.Slice<number>) => {
1445
+ if (offset >= data.length) {
1446
+ return [0, io.EOF] as [number, $.GoError]
1447
+ }
1448
+ const n = Math.min(p?.length ?? 0, data.length - offset)
1449
+ p?.set(data.subarray(offset, offset + n), 0)
1450
+ offset += n
1451
+ return [n, null] as [number, $.GoError]
1452
+ },
1453
+ Stat: async () => [
1454
+ {
1455
+ Name: () => name,
1456
+ Size: () => data.length,
1457
+ Mode: () => 0,
1458
+ ModTime: () => new time.Time(),
1459
+ IsDir: () => false,
1460
+ Sys: () => null,
1461
+ },
1462
+ null,
1463
+ ],
1464
+ },
1465
+ null,
1466
+ ]
1467
+ },
1468
+ }
1469
+ const writes: string[] = []
1470
+ const header = new Header()
1471
+ const writer: ResponseWriter = {
1472
+ Header: () => header,
1473
+ Write: (p) => {
1474
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
1475
+ return [p?.length ?? 0, null]
1476
+ },
1477
+ WriteHeader: (code) => writes.push(`status:${code}`),
1478
+ }
1479
+ const [req] = NewRequest(
1480
+ MethodGet,
1481
+ 'http://example.invalid/dir/fs-adapter.txt',
1482
+ null,
1483
+ )
1484
+
1485
+ await FileServerFS(root).ServeHTTP(writer, req)
1486
+
1487
+ expect(opened).toEqual(['dir/fs-adapter.txt'])
1488
+ expect(writes).toEqual(['status:200', 'fs-adapter'])
1489
+ expect(Header_Get(header, 'Content-Length')).toBe('10')
1490
+ })
1491
+
1492
+ it('serves files through async response writer adapters', async () => {
1493
+ const root = {
1494
+ async Open(name: string) {
1495
+ const data = $.stringToBytes('async-adapter')
1496
+ return [
1497
+ {
1498
+ Read: (p: $.Slice<number>) => {
1499
+ p?.set(data.subarray(0, p.length))
1500
+ return [data.length, io.EOF] as [number, $.GoError]
1501
+ },
1502
+ Close: async () => null,
1503
+ Stat: async () => [
1504
+ {
1505
+ Name: () => name,
1506
+ Size: () => data.length,
1507
+ Mode: () => 0,
1508
+ ModTime: () => new time.Time(),
1509
+ IsDir: () => false,
1510
+ Sys: () => null,
1511
+ },
1512
+ null,
1513
+ ],
1514
+ },
1515
+ null,
1516
+ ] as [File, $.GoError]
1517
+ },
1518
+ }
1519
+ const writes: string[] = []
1520
+ const header = new Header()
1521
+ const writer: ResponseWriter = {
1522
+ Header: async () => header,
1523
+ Write: async (p) => {
1524
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
1525
+ return [p?.length ?? 0, null]
1526
+ },
1527
+ WriteHeader: async (code) => {
1528
+ writes.push(`status:${code}`)
1529
+ },
1530
+ }
1531
+ const [req] = NewRequest(
1532
+ MethodGet,
1533
+ 'http://example.invalid/async-adapter.txt',
1534
+ null,
1535
+ )
1536
+
1537
+ await FileServer(root).ServeHTTP(writer, req)
1538
+
1539
+ expect(writes).toEqual(['status:200', 'async-adapter'])
1540
+ expect(Header_Get(header, 'Content-Length')).toBe('13')
1541
+ })
1542
+
1543
+ it('writes file server headers before reading response bodies', async () => {
1544
+ let headerWritten = false
1545
+ let readSawHeader = false
1546
+ let offset = 0
1547
+ const data = $.stringToBytes('streamed-body')
1548
+ const root = {
1549
+ async Open(name: string) {
1550
+ return [
1551
+ {
1552
+ Close: async () => null,
1553
+ Read: async (p: $.Slice<number>) => {
1554
+ readSawHeader = headerWritten
1555
+ if (offset >= data.length) {
1556
+ return [0, io.EOF] as [number, $.GoError]
1557
+ }
1558
+ const n = Math.min(p?.length ?? 0, data.length - offset)
1559
+ p?.set(data.subarray(offset, offset + n), 0)
1560
+ offset += n
1561
+ return [n, null] as [number, $.GoError]
1562
+ },
1563
+ Seek: async () => [0, null] as [number, $.GoError],
1564
+ Readdir: async () => [null, null] as [null, $.GoError],
1565
+ Stat: async () => [
1566
+ {
1567
+ Name: () => name,
1568
+ Size: () => data.length,
1569
+ Mode: () => 0,
1570
+ ModTime: () => null as never,
1571
+ IsDir: () => false,
1572
+ Sys: () => null,
1573
+ },
1574
+ null,
1575
+ ],
1576
+ },
1577
+ null,
1578
+ ] as [File, $.GoError]
1579
+ },
1580
+ }
1581
+ const writes: string[] = []
1582
+ const header = new Header()
1583
+ const writer: ResponseWriter = {
1584
+ Header: () => header,
1585
+ Write: (p) => {
1586
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
1587
+ return [p?.length ?? 0, null]
1588
+ },
1589
+ WriteHeader: (code) => {
1590
+ headerWritten = true
1591
+ writes.push(`status:${code}`)
1592
+ },
1593
+ }
1594
+ const [req] = NewRequest(
1595
+ MethodGet,
1596
+ 'http://example.invalid/streamed.mjs',
1597
+ null,
1598
+ )
1599
+
1600
+ await FileServer(root).ServeHTTP(writer, req)
1601
+
1602
+ expect(readSawHeader).toBe(true)
1603
+ expect(writes).toEqual(['status:200', 'streamed-body'])
1604
+ expect(Header_Get(header, 'Content-Type')).toBe('text/javascript; charset=utf-8')
1605
+ })
1606
+
904
1607
  it('awaits ServeContent writes before returning', async () => {
905
1608
  const writes: string[] = []
906
1609
  const writer: ResponseWriter = {
@@ -911,15 +1614,35 @@ describe('net/http override', () => {
911
1614
  },
912
1615
  WriteHeader: (code) => writes.push(`status:${code}`),
913
1616
  }
914
- const [req] = NewRequest(MethodGet, 'http://example.invalid/content.txt', null)
1617
+ const [req] = NewRequest(
1618
+ MethodGet,
1619
+ 'http://example.invalid/content.txt',
1620
+ null,
1621
+ )
915
1622
 
916
- await ServeContent(writer, req, 'content.txt', null as never, bytes.NewReader($.stringToBytes('served')))
1623
+ await ServeContent(
1624
+ writer,
1625
+ req,
1626
+ 'content.txt',
1627
+ null as never,
1628
+ bytes.NewReader($.stringToBytes('served')),
1629
+ )
917
1630
 
918
1631
  expect(writes).toEqual(['status:200', 'served'])
919
1632
 
920
1633
  writes.length = 0
921
- const [headReq] = NewRequest(MethodHead, 'http://example.invalid/content.txt', null)
922
- await ServeContent(writer, headReq, 'content.txt', null as never, bytes.NewReader($.stringToBytes('hidden')))
1634
+ const [headReq] = NewRequest(
1635
+ MethodHead,
1636
+ 'http://example.invalid/content.txt',
1637
+ null,
1638
+ )
1639
+ await ServeContent(
1640
+ writer,
1641
+ headReq,
1642
+ 'content.txt',
1643
+ null as never,
1644
+ bytes.NewReader($.stringToBytes('hidden')),
1645
+ )
923
1646
 
924
1647
  expect(writes).toEqual(['status:200'])
925
1648
  })
@@ -954,7 +1677,11 @@ describe('net/http override', () => {
954
1677
  },
955
1678
  WriteHeader: (code) => writes.push(`status:${code}`),
956
1679
  }
957
- const [req] = NewRequest(MethodGet, 'http://example.invalid/eval/missing.js', null)
1680
+ const [req] = NewRequest(
1681
+ MethodGet,
1682
+ 'http://example.invalid/eval/missing.js',
1683
+ null,
1684
+ )
958
1685
 
959
1686
  ServeFile(writer, req, '/host-only/missing.js')
960
1687