goscript 0.1.2 → 0.1.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 (138) hide show
  1. package/cmd/goscript/cmd_compile.go +28 -8
  2. package/cmd/goscript/cmd_compile_test.go +105 -6
  3. package/compiler/build-flags.go +9 -10
  4. package/compiler/gotest/runner_test.go +127 -0
  5. package/compiler/lowered-program.go +1 -0
  6. package/compiler/lowering.go +1325 -194
  7. package/compiler/lowering_bench_test.go +350 -0
  8. package/compiler/override-registry_test.go +43 -0
  9. package/compiler/package-graph.go +61 -4
  10. package/compiler/package-graph_test.go +30 -0
  11. package/compiler/semantic-model-types.go +8 -0
  12. package/compiler/semantic-model.go +447 -22
  13. package/compiler/semantic-model_test.go +138 -0
  14. package/compiler/skeleton_test.go +1436 -50
  15. package/compiler/typescript-emitter.go +47 -4
  16. package/dist/gs/builtin/builtin.d.ts +2 -2
  17. package/dist/gs/builtin/builtin.js +20 -0
  18. package/dist/gs/builtin/builtin.js.map +1 -1
  19. package/dist/gs/builtin/channel.js +36 -9
  20. package/dist/gs/builtin/channel.js.map +1 -1
  21. package/dist/gs/builtin/slice.js +5 -0
  22. package/dist/gs/builtin/slice.js.map +1 -1
  23. package/dist/gs/builtin/type.d.ts +1 -1
  24. package/dist/gs/builtin/type.js +80 -8
  25. package/dist/gs/builtin/type.js.map +1 -1
  26. package/dist/gs/bytes/bytes.gs.d.ts +7 -5
  27. package/dist/gs/bytes/bytes.gs.js +10 -4
  28. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  29. package/dist/gs/compress/zlib/index.d.ts +3 -3
  30. package/dist/gs/compress/zlib/index.js +88 -26
  31. package/dist/gs/compress/zlib/index.js.map +1 -1
  32. package/dist/gs/crypto/sha1/index.d.ts +5 -0
  33. package/dist/gs/crypto/sha1/index.js +103 -0
  34. package/dist/gs/crypto/sha1/index.js.map +1 -0
  35. package/dist/gs/crypto/sha256/index.js +2 -5
  36. package/dist/gs/crypto/sha256/index.js.map +1 -1
  37. package/dist/gs/crypto/sha512/index.js +2 -5
  38. package/dist/gs/crypto/sha512/index.js.map +1 -1
  39. package/dist/gs/embed/index.d.ts +6 -0
  40. package/dist/gs/embed/index.js +210 -5
  41. package/dist/gs/embed/index.js.map +1 -1
  42. package/dist/gs/fmt/fmt.d.ts +4 -4
  43. package/dist/gs/fmt/fmt.js +93 -19
  44. package/dist/gs/fmt/fmt.js.map +1 -1
  45. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
  46. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
  47. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
  48. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
  49. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
  50. package/dist/gs/io/fs/readdir.js +5 -3
  51. package/dist/gs/io/fs/readdir.js.map +1 -1
  52. package/dist/gs/io/io.d.ts +18 -11
  53. package/dist/gs/io/io.js +107 -44
  54. package/dist/gs/io/io.js.map +1 -1
  55. package/dist/gs/math/bits/index.d.ts +26 -5
  56. package/dist/gs/math/bits/index.js +13 -24
  57. package/dist/gs/math/bits/index.js.map +1 -1
  58. package/dist/gs/net/http/httptest/index.js +7 -5
  59. package/dist/gs/net/http/httptest/index.js.map +1 -1
  60. package/dist/gs/net/http/index.d.ts +11 -1
  61. package/dist/gs/net/http/index.js +157 -11
  62. package/dist/gs/net/http/index.js.map +1 -1
  63. package/dist/gs/os/types_js.gs.d.ts +6 -2
  64. package/dist/gs/os/types_js.gs.js +169 -8
  65. package/dist/gs/os/types_js.gs.js.map +1 -1
  66. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  67. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  68. package/dist/gs/reflect/type.d.ts +1 -0
  69. package/dist/gs/reflect/type.js +80 -51
  70. package/dist/gs/reflect/type.js.map +1 -1
  71. package/dist/gs/strings/reader.d.ts +1 -1
  72. package/dist/gs/strings/reader.js +2 -2
  73. package/dist/gs/strings/reader.js.map +1 -1
  74. package/dist/gs/sync/sync.d.ts +2 -1
  75. package/dist/gs/sync/sync.js +37 -16
  76. package/dist/gs/sync/sync.js.map +1 -1
  77. package/dist/gs/syscall/js/index.js +9 -0
  78. package/dist/gs/syscall/js/index.js.map +1 -1
  79. package/dist/gs/testing/testing.js +8 -6
  80. package/dist/gs/testing/testing.js.map +1 -1
  81. package/gs/builtin/builtin.ts +25 -2
  82. package/gs/builtin/channel.ts +47 -9
  83. package/gs/builtin/runtime-contract.test.ts +78 -0
  84. package/gs/builtin/slice.ts +7 -0
  85. package/gs/builtin/type.ts +97 -8
  86. package/gs/bytes/bytes.gs.ts +19 -10
  87. package/gs/bytes/bytes.test.ts +17 -0
  88. package/gs/compress/zlib/index.test.ts +97 -0
  89. package/gs/compress/zlib/index.ts +117 -27
  90. package/gs/compress/zlib/meta.json +4 -1
  91. package/gs/context/context.test.ts +5 -1
  92. package/gs/crypto/sha1/index.test.ts +45 -0
  93. package/gs/crypto/sha1/index.ts +127 -0
  94. package/gs/crypto/sha1/meta.json +8 -0
  95. package/gs/crypto/sha256/index.test.ts +14 -2
  96. package/gs/crypto/sha256/index.ts +3 -6
  97. package/gs/crypto/sha512/index.test.ts +17 -2
  98. package/gs/crypto/sha512/index.ts +3 -6
  99. package/gs/embed/index.test.ts +87 -0
  100. package/gs/embed/index.ts +229 -5
  101. package/gs/fmt/fmt.test.ts +61 -3
  102. package/gs/fmt/fmt.ts +115 -22
  103. package/gs/fmt/meta.json +6 -1
  104. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
  105. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
  106. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
  107. package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
  108. package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
  109. package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
  110. package/gs/io/fs/readdir.test.ts +38 -0
  111. package/gs/io/fs/readdir.ts +7 -3
  112. package/gs/io/io.test.ts +135 -0
  113. package/gs/io/io.ts +143 -63
  114. package/gs/io/meta.json +7 -1
  115. package/gs/math/bits/index.ts +52 -28
  116. package/gs/net/http/httptest/index.test.ts +34 -2
  117. package/gs/net/http/httptest/index.ts +23 -8
  118. package/gs/net/http/index.test.ts +46 -0
  119. package/gs/net/http/index.ts +178 -12
  120. package/gs/os/file_unix_js.test.ts +52 -0
  121. package/gs/os/meta.json +4 -0
  122. package/gs/os/readdir.test.ts +56 -0
  123. package/gs/os/types_js.gs.ts +169 -8
  124. package/gs/os/zero_copy_posix.gs.ts +1 -2
  125. package/gs/reflect/deepequal.test.ts +10 -1
  126. package/gs/reflect/type.ts +91 -56
  127. package/gs/reflect/typefor.test.ts +31 -1
  128. package/gs/strings/meta.json +5 -2
  129. package/gs/strings/reader.test.ts +2 -2
  130. package/gs/strings/reader.ts +2 -2
  131. package/gs/sync/meta.json +2 -0
  132. package/gs/sync/sync.test.ts +41 -1
  133. package/gs/sync/sync.ts +41 -16
  134. package/gs/syscall/js/index.test.ts +18 -0
  135. package/gs/syscall/js/index.ts +12 -0
  136. package/gs/testing/testing.test.ts +32 -3
  137. package/gs/testing/testing.ts +13 -10
  138. package/package.json +1 -1
@@ -2,7 +2,14 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import * as $ from '@goscript/builtin/index.js'
4
4
 
5
- import { Handler, Header_Get, Header_Set, MethodGet, NewRequest, StatusPartialContent } from '../index.js'
5
+ import {
6
+ Handler,
7
+ Header_Get,
8
+ Header_Set,
9
+ MethodGet,
10
+ NewRequest,
11
+ StatusPartialContent,
12
+ } from '../index.js'
6
13
  import { NewServer, NewUnstartedServer, Server_Start } from './index.js'
7
14
 
8
15
  describe('net/http/httptest override', () => {
@@ -12,12 +19,13 @@ describe('net/http/httptest override', () => {
12
19
  }
13
20
 
14
21
  const srv = NewServer(handler)
15
- expect(srv.URL).toBe('http://127.0.0.1')
22
+ expect(srv.URL).toMatch(/^http:\/\/goscript-httptest-\d+\.invalid$/)
16
23
  expect(srv.Client()).toBeTruthy()
17
24
  expect(srv.Config().Handler).toBe(handler)
18
25
  expect(Server_Start(NewUnstartedServer(handler))?.Error()).toBe(
19
26
  'net/http/httptest: Server.Start is not implemented in GoScript',
20
27
  )
28
+ srv.Close()
21
29
  })
22
30
 
23
31
  it('routes Client.Do through the in-memory server handler', async () => {
@@ -49,5 +57,29 @@ describe('net/http/httptest override', () => {
49
57
  expect(n).toBe(4)
50
58
  expect(Buffer.from(buf).toString('utf8')).toBe('data')
51
59
  expect(resp!.Body!.Close()).toBeNull()
60
+ srv.Close()
61
+ })
62
+
63
+ it('awaits async handlers through Server.Client transport', async () => {
64
+ const srv = NewServer({
65
+ async ServeHTTP(w) {
66
+ await Promise.resolve()
67
+ w!.WriteHeader(StatusPartialContent)
68
+ w!.Write($.stringToBytes('async'))
69
+ },
70
+ })
71
+ const [req, reqErr] = NewRequest(MethodGet, srv.URL + '/async', null)
72
+ expect(reqErr).toBeNull()
73
+
74
+ const [resp, err] = await srv.Client().Do(req)
75
+
76
+ expect(err).toBeNull()
77
+ expect(resp?.StatusCode).toBe(StatusPartialContent)
78
+ const buf = new Uint8Array(5)
79
+ const [n, readErr] = resp!.Body!.Read(buf)
80
+ expect(readErr).toBeNull()
81
+ expect(n).toBe(5)
82
+ expect(Buffer.from(buf).toString('utf8')).toBe('async')
83
+ srv.Close()
52
84
  })
53
85
  })
@@ -58,10 +58,16 @@ export function NewRecorder(): ResponseRecorder {
58
58
  return new ResponseRecorder()
59
59
  }
60
60
 
61
- export function NewRequest(method: string, target: string, body: io.Reader | null): http.Request {
61
+ export function NewRequest(
62
+ method: string,
63
+ target: string,
64
+ body: io.Reader | null,
65
+ ): http.Request {
62
66
  const [req, err] = http.NewRequest(method, target, body)
63
67
  if (err != null || req == null) {
64
- throw err ?? errors.New('net/http/httptest: NewRequest returned nil request')
68
+ throw (
69
+ err ?? errors.New('net/http/httptest: NewRequest returned nil request')
70
+ )
65
71
  }
66
72
  return req
67
73
  }
@@ -71,21 +77,26 @@ export class Server {
71
77
  private handler: http.Handler | null
72
78
 
73
79
  constructor(init?: Partial<Server> & { Handler?: http.Handler | null }) {
74
- this.URL = init?.URL ?? 'http://127.0.0.1'
75
80
  this.handler = init?.Handler ?? null
81
+ this.URL = init?.URL ?? http.RegisterInProcessServer(this.handler)
76
82
  }
77
83
 
78
84
  public Client(): http.Client {
79
85
  return new http.Client({ Transport: new serverTransport(this) })
80
86
  }
81
87
 
82
- public Close(): void {}
88
+ public Close(): void {
89
+ http.UnregisterInProcessServer(this.URL)
90
+ }
83
91
 
84
92
  public Config(): http.Server {
85
93
  return new http.Server({ Handler: this.handler })
86
94
  }
87
95
 
88
- public ServeHTTP(w: http.ResponseWriter | null, r: http.Request | $.VarRef<http.Request> | null): void | Promise<void> {
96
+ public ServeHTTP(
97
+ w: http.ResponseWriter | null,
98
+ r: http.Request | $.VarRef<http.Request> | null,
99
+ ): void | Promise<void> {
89
100
  return this.handler?.ServeHTTP(w, r)
90
101
  }
91
102
  }
@@ -93,7 +104,9 @@ export class Server {
93
104
  class serverTransport implements http.RoundTripper {
94
105
  constructor(private server: Server) {}
95
106
 
96
- public RoundTrip(req: http.Request | $.VarRef<http.Request> | null): [http.Response | null, $.GoError] {
107
+ public async RoundTrip(
108
+ req: http.Request | $.VarRef<http.Request> | null,
109
+ ): Promise<[http.Response | null, $.GoError]> {
97
110
  const request = $.pointerValue<http.Request | null>(req)
98
111
  if (request == null) {
99
112
  return [null, errors.New('net/http: nil Request')]
@@ -101,7 +114,7 @@ class serverTransport implements http.RoundTripper {
101
114
  const recorder = NewRecorder()
102
115
  const served = this.server.ServeHTTP(recorder, request)
103
116
  if (served instanceof Promise) {
104
- return [null, errors.New('net/http/httptest: async handlers are not supported by Client.Do')]
117
+ await served
105
118
  }
106
119
  return [recorder.Result(), null]
107
120
  }
@@ -116,5 +129,7 @@ export function NewUnstartedServer(handler: http.Handler | null): Server {
116
129
  }
117
130
 
118
131
  export function Server_Start(_s: Server | $.VarRef<Server> | null): $.GoError {
119
- return errors.New('net/http/httptest: Server.Start is not implemented in GoScript')
132
+ return errors.New(
133
+ 'net/http/httptest: Server.Start is not implemented in GoScript',
134
+ )
120
135
  }
@@ -7,6 +7,9 @@ import {
7
7
  File,
8
8
  FileSystem,
9
9
  Header,
10
+ Header_Add,
11
+ Header_Del,
12
+ Header_Get,
10
13
  Header_Set,
11
14
  HandlerFunc_ServeHTTP,
12
15
  Get,
@@ -19,11 +22,16 @@ import {
19
22
  ResponseWriter,
20
23
  Server,
21
24
  StatusCreated,
25
+ StatusForbidden,
26
+ StatusMethodNotAllowed,
22
27
  StatusNotFound,
23
28
  StatusOK,
24
29
  StatusServiceUnavailable,
30
+ StatusTeapot,
25
31
  StatusText,
26
32
  StatusTooManyRequests,
33
+ StatusUnauthorized,
34
+ StatusUnsupportedMediaType,
27
35
  } from './index.js'
28
36
 
29
37
  describe('net/http override', () => {
@@ -33,6 +41,11 @@ describe('net/http override', () => {
33
41
  expect(resp.StatusCode).toBe(200)
34
42
  expect(StatusText(resp.StatusCode)).toBe('OK')
35
43
  expect(StatusText(StatusNotFound)).toBe('Not Found')
44
+ expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
45
+ expect(StatusText(StatusForbidden)).toBe('Forbidden')
46
+ expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
47
+ expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
48
+ expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
36
49
  expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
37
50
  expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
38
51
  expect(StatusText(599)).toBe('')
@@ -59,6 +72,22 @@ describe('net/http override', () => {
59
72
  expect(err?.Error()).toBe('net/http: Client.Do is not implemented in GoScript')
60
73
  })
61
74
 
75
+ it('wraps request body readers and keeps response metadata', () => {
76
+ const reader = {
77
+ Read: (p: Uint8Array) => [p.length, null] as [number, null],
78
+ }
79
+
80
+ const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', reader)
81
+
82
+ expect(reqErr).toBeNull()
83
+ expect(req!.Body).not.toBeNull()
84
+ expect(req!.Body!.Close()).toBeNull()
85
+
86
+ const resp = new Response({ StatusCode: StatusCreated, ContentLength: -1, Request: varRef(req!) })
87
+ expect(resp.ContentLength).toBe(-1)
88
+ expect((resp.Request as any).value).toBe(req)
89
+ })
90
+
62
91
  it('exports the default transport surface', async () => {
63
92
  const [req] = NewRequest(MethodPost, 'https://example.invalid', null)
64
93
 
@@ -92,6 +121,23 @@ describe('net/http override', () => {
92
121
  expect(resp?.StatusCode).toBe(StatusOK)
93
122
  })
94
123
 
124
+ it('canonicalizes header keys for case-insensitive lookup', () => {
125
+ const header = new Header()
126
+
127
+ Header_Set(header, 'Content-Type', 'application/json')
128
+
129
+ expect(header.has('Content-Type')).toBe(true)
130
+ expect(header.has('content-type')).toBe(false)
131
+ expect(Header_Get(header, 'content-type')).toBe('application/json')
132
+
133
+ Header_Add(header, 'x-pack-id', 'pack-1')
134
+ Header_Add(header, 'X-Pack-Id', 'pack-2')
135
+ expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual(['pack-1', 'pack-2'])
136
+
137
+ Header_Del(header, 'x-pack-id')
138
+ expect(Header_Get(header, 'X-Pack-ID')).toBe('')
139
+ })
140
+
95
141
  it('accepts server context and shutdown surfaces', () => {
96
142
  const srv = new Server({
97
143
  Addr: ':0',
@@ -1,4 +1,5 @@
1
1
  import * as $ from '@goscript/builtin/index.js'
2
+ import * as bytes from '@goscript/bytes/index.js'
2
3
  import * as context from '@goscript/context/index.js'
3
4
  import * as errors from '@goscript/errors/index.js'
4
5
  import * as fs from '@goscript/io/fs/fs.js'
@@ -10,9 +11,14 @@ export const StatusCreated = 201
10
11
  export const StatusPartialContent = 206
11
12
  export const StatusMovedPermanently = 301
12
13
  export const StatusBadRequest = 400
14
+ export const StatusUnauthorized = 401
15
+ export const StatusForbidden = 403
16
+ export const StatusMethodNotAllowed = 405
13
17
  export const StatusRequestTimeout = 408
14
18
  export const StatusConflict = 409
15
19
  export const StatusNotFound = 404
20
+ export const StatusUnsupportedMediaType = 415
21
+ export const StatusTeapot = 418
16
22
  export const StatusTooManyRequests = 429
17
23
  export const StatusRequestedRangeNotSatisfiable = 416
18
24
  export const StatusInternalServerError = 500
@@ -31,6 +37,12 @@ export function StatusText(code: number): string {
31
37
  return 'OK'
32
38
  case StatusMovedPermanently:
33
39
  return 'Moved Permanently'
40
+ case StatusUnauthorized:
41
+ return 'Unauthorized'
42
+ case StatusForbidden:
43
+ return 'Forbidden'
44
+ case StatusMethodNotAllowed:
45
+ return 'Method Not Allowed'
34
46
  case StatusBadRequest:
35
47
  return 'Bad Request'
36
48
  case StatusRequestTimeout:
@@ -39,12 +51,16 @@ export function StatusText(code: number): string {
39
51
  return 'Conflict'
40
52
  case StatusNotFound:
41
53
  return 'Not Found'
54
+ case StatusUnsupportedMediaType:
55
+ return 'Unsupported Media Type'
42
56
  case StatusTooManyRequests:
43
57
  return 'Too Many Requests'
44
58
  case StatusPartialContent:
45
59
  return 'Partial Content'
46
60
  case StatusRequestedRangeNotSatisfiable:
47
61
  return 'Requested Range Not Satisfiable'
62
+ case StatusTeapot:
63
+ return "I'm a teapot"
48
64
  case StatusInternalServerError:
49
65
  return 'Internal Server Error'
50
66
  case StatusServiceUnavailable:
@@ -61,22 +77,39 @@ export const Header = Map as {
61
77
  }
62
78
 
63
79
  export function Header_Add(h: Header, key: string, value: string): void {
80
+ key = canonicalMIMEHeaderKey(key)
64
81
  const values = Array.from(h.get(key) ?? [])
65
82
  values.push(value)
66
83
  h.set(key, $.arrayToSlice(values))
67
84
  }
68
85
 
69
86
  export function Header_Del(h: Header, key: string): void {
70
- h.delete(key)
87
+ h.delete(canonicalMIMEHeaderKey(key))
71
88
  }
72
89
 
73
90
  export function Header_Get(h: Header, key: string): string {
74
- const values = h.get(key)
91
+ const values = h.get(canonicalMIMEHeaderKey(key))
75
92
  return values == null || values.length === 0 ? '' : String(values[0])
76
93
  }
77
94
 
78
95
  export function Header_Set(h: Header, key: string, value: string): void {
79
- h.set(key, $.arrayToSlice([value]))
96
+ h.set(canonicalMIMEHeaderKey(key), $.arrayToSlice([value]))
97
+ }
98
+
99
+ function canonicalMIMEHeaderKey(key: string): string {
100
+ let upper = true
101
+ let out = ''
102
+ for (let i = 0; i < key.length; i++) {
103
+ const ch = key[i]
104
+ if (ch === '-') {
105
+ upper = true
106
+ out += ch
107
+ continue
108
+ }
109
+ out += upper ? ch.toUpperCase() : ch.toLowerCase()
110
+ upper = false
111
+ }
112
+ return out
80
113
  }
81
114
 
82
115
  class QueryValues extends Map<string, $.Slice<string>> {
@@ -90,13 +123,27 @@ class QueryValues extends Map<string, $.Slice<string>> {
90
123
  const values = this.get(key)
91
124
  return values == null || values.length === 0 ? '' : String(values[0])
92
125
  }
126
+
127
+ public Encode(): string {
128
+ const params = new URLSearchParams()
129
+ for (const [key, values] of this.entries()) {
130
+ for (const value of Array.from(values ?? [])) {
131
+ params.append(key, String(value))
132
+ }
133
+ }
134
+ return params.toString()
135
+ }
93
136
  }
94
137
 
95
138
  class RequestURL {
139
+ public Scheme: string
140
+ public Host: string
96
141
  public Path: string
97
142
  public RawQuery: string
98
143
 
99
- constructor(path: string, rawQuery: string) {
144
+ constructor(path: string, rawQuery: string, scheme = '', host = '') {
145
+ this.Scheme = scheme
146
+ this.Host = host
100
147
  this.Path = path
101
148
  this.RawQuery = rawQuery
102
149
  }
@@ -109,19 +156,100 @@ class RequestURL {
109
156
  }
110
157
 
111
158
  public clone(): RequestURL {
112
- return new RequestURL(this.Path, this.RawQuery)
159
+ return new RequestURL(this.Path, this.RawQuery, this.Scheme, this.Host)
160
+ }
161
+
162
+ public String(): string {
163
+ const query = this.RawQuery === '' ? '' : `?${this.RawQuery}`
164
+ const path = this.Path === '' ? '/' : this.Path
165
+ if (this.Scheme === '' || this.Host === '') {
166
+ return `${path}${query}`
167
+ }
168
+ return `${this.Scheme}://${this.Host}${path}${query}`
113
169
  }
114
170
  }
115
171
 
116
172
  function parseRequestURL(rawURL: string): RequestURL {
117
173
  try {
118
174
  const parsed = new URL(rawURL, 'http://goscript.invalid')
119
- return new RequestURL(parsed.pathname, parsed.search.startsWith('?') ? parsed.search.slice(1) : parsed.search)
175
+ const hasHost = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(rawURL)
176
+ return new RequestURL(
177
+ parsed.pathname,
178
+ parsed.search.startsWith('?') ? parsed.search.slice(1) : parsed.search,
179
+ hasHost ? parsed.protocol.replace(/:$/, '') : '',
180
+ hasHost ? parsed.host : '',
181
+ )
120
182
  } catch {
121
183
  return new RequestURL('', '')
122
184
  }
123
185
  }
124
186
 
187
+ class responseBody implements io.ReadCloser {
188
+ private reader: bytes.Reader
189
+
190
+ constructor(data: $.Bytes) {
191
+ this.reader = bytes.NewReader(Uint8Array.from(data ?? []))
192
+ }
193
+
194
+ public Read(p: $.Bytes): [number, $.GoError] {
195
+ return this.reader.Read(p)
196
+ }
197
+
198
+ public Close(): $.GoError {
199
+ return null
200
+ }
201
+ }
202
+
203
+ class memoryResponseWriter implements ResponseWriter {
204
+ public Code = StatusOK
205
+ public Body = new bytes.Buffer()
206
+ private headerMap = new Header()
207
+ private wroteHeader = false
208
+
209
+ public Header(): Header {
210
+ return this.headerMap
211
+ }
212
+
213
+ public Write(p: $.Slice<number>): [number, $.GoError] {
214
+ if (!this.wroteHeader) {
215
+ this.WriteHeader(StatusOK)
216
+ }
217
+ return this.Body.Write(p)
218
+ }
219
+
220
+ public WriteHeader(statusCode: number): void {
221
+ if (this.wroteHeader) {
222
+ return
223
+ }
224
+ this.wroteHeader = true
225
+ this.Code = statusCode
226
+ }
227
+
228
+ public Result(): Response {
229
+ return new Response({
230
+ Body: new responseBody(this.Body.Bytes()),
231
+ Header: this.headerMap,
232
+ StatusCode: this.Code,
233
+ })
234
+ }
235
+ }
236
+
237
+ const inProcessServers = new Map<string, Handler>()
238
+ let nextInProcessServerID = 1
239
+
240
+ export function RegisterInProcessServer(handler: Handler | null): string {
241
+ const host = `goscript-httptest-${nextInProcessServerID++}.invalid`
242
+ inProcessServers.set(host, handler ?? { ServeHTTP: NotFound })
243
+ return `http://${host}`
244
+ }
245
+
246
+ export function UnregisterInProcessServer(rawURL: string): void {
247
+ const parsed = parseRequestURL(rawURL)
248
+ if (parsed.Host !== '') {
249
+ inProcessServers.delete(parsed.Host)
250
+ }
251
+ }
252
+
125
253
  export interface ResponseWriter {
126
254
  Header(): Header
127
255
  Write(p: $.Slice<number>): [number, $.GoError]
@@ -131,7 +259,7 @@ export interface ResponseWriter {
131
259
  export class Request {
132
260
  public Method: string
133
261
  public URL: any
134
- public Body: io.Reader | null
262
+ public Body: io.ReadCloser | null
135
263
  public Header: Header
136
264
  public ContentLength: number
137
265
  public RequestURI: string
@@ -181,21 +309,34 @@ export class Request {
181
309
  }
182
310
 
183
311
  export class Response {
312
+ public Status: string
184
313
  public StatusCode: number
185
314
  public Body: io.ReadCloser | null
186
315
  public Header: Header
316
+ public ContentLength: number
317
+ public Request: Request | $.VarRef<Request> | null
187
318
 
188
319
  constructor(init?: Partial<Response>) {
320
+ this.Status = init?.Status ?? ''
189
321
  this.StatusCode = init?.StatusCode ?? 0
190
322
  this.Body = init?.Body ?? null
191
323
  this.Header = init?.Header ?? new Header()
324
+ this.ContentLength = init?.ContentLength ?? 0
325
+ this.Request = init?.Request ?? null
326
+ if (this.Status === '' && this.StatusCode !== 0) {
327
+ const text = StatusText(this.StatusCode)
328
+ this.Status = text === '' ? String(this.StatusCode) : `${this.StatusCode} ${text}`
329
+ }
192
330
  }
193
331
 
194
332
  public clone(): Response {
195
333
  return new Response({
196
334
  Body: this.Body,
197
335
  Header: this.Header,
336
+ Status: this.Status,
198
337
  StatusCode: this.StatusCode,
338
+ ContentLength: this.ContentLength,
339
+ Request: this.Request,
199
340
  })
200
341
  }
201
342
  }
@@ -220,13 +361,27 @@ export interface RoundTripper {
220
361
  RoundTrip(req: Request | $.VarRef<Request> | null): [Response | null, $.GoError] | Promise<[Response | null, $.GoError]>
221
362
  }
222
363
 
223
- class unsupportedTransport implements RoundTripper {
224
- public async RoundTrip(_req: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
225
- return [null, errors.New('net/http: Client.Do is not implemented in GoScript')]
364
+ class defaultTransport implements RoundTripper {
365
+ public async RoundTrip(req: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
366
+ const request = $.pointerValue<Request | null>(req)
367
+ if (request == null) {
368
+ return [null, errors.New('net/http: nil Request')]
369
+ }
370
+ const host = request.URL?.Host ?? ''
371
+ const handler = host === '' ? null : inProcessServers.get(host)
372
+ if (handler == null) {
373
+ return [null, errors.New('net/http: Client.Do is not implemented in GoScript')]
374
+ }
375
+ const recorder = new memoryResponseWriter()
376
+ const served = handler.ServeHTTP(recorder, request)
377
+ if (served instanceof Promise) {
378
+ await served
379
+ }
380
+ return [recorder.Result(), null]
226
381
  }
227
382
  }
228
383
 
229
- export const DefaultTransport: RoundTripper = new unsupportedTransport()
384
+ export const DefaultTransport: RoundTripper = new defaultTransport()
230
385
 
231
386
  export interface FileSystem {
232
387
  Open(name: string): [File | null, $.GoError]
@@ -424,9 +579,20 @@ export function NewRequestWithContext(
424
579
  method = MethodGet
425
580
  }
426
581
  const parsedURL = parseRequestURL(url)
427
- return [new Request({ Method: method, URL: parsedURL, Body: body, RequestURI: parsedURL.Path, ctx: _ctx }), null]
582
+ return [new Request({ Method: method, URL: parsedURL, Body: readCloserForBody(body), RequestURI: parsedURL.Path, ctx: _ctx }), null]
428
583
  }
429
584
 
430
585
  export function Get(_url: string): [Response | null, $.GoError] {
431
586
  return [null, errors.New('net/http: Get is not implemented in GoScript')]
432
587
  }
588
+
589
+ function readCloserForBody(body: io.Reader | null): io.ReadCloser | null {
590
+ if (body == null) {
591
+ return null
592
+ }
593
+ const closer = body as io.Reader & Partial<io.Closer>
594
+ if (typeof closer.Close === 'function') {
595
+ return closer as io.ReadCloser
596
+ }
597
+ return io.NopCloser(body)
598
+ }
@@ -286,4 +286,56 @@ describe('os stdio', () => {
286
286
  expect(readSync).toHaveBeenCalledTimes(1)
287
287
  expect(writeSync).toHaveBeenCalledTimes(1)
288
288
  })
289
+
290
+ it('copies from File without recursing through io.Copy', async () => {
291
+ const readSync = vi
292
+ .fn<(buffer: Uint8Array) => number>()
293
+ .mockImplementationOnce((buffer) => {
294
+ buffer.set([65, 66, 67], 0)
295
+ return 3
296
+ })
297
+ .mockImplementationOnce(() => 0)
298
+
299
+ const file = createHostFile('descriptor-file', 7, { readSync })
300
+ const chunks: number[] = []
301
+ const writer = {
302
+ Write(p: Uint8Array): [number, null] {
303
+ chunks.push(...Array.from(p))
304
+ return [p.length, null]
305
+ },
306
+ }
307
+
308
+ const [n, err] = await io.Copy(writer, file)
309
+
310
+ expect(err).toBeNull()
311
+ expect(n).toBe(3)
312
+ expect(Buffer.from(chunks).toString('utf8')).toBe('ABC')
313
+ })
314
+
315
+ it('copies into File without recursing through io.Copy', async () => {
316
+ const writes: number[] = []
317
+ const writeSync = vi.fn((buffer: Uint8Array) => {
318
+ writes.push(...Array.from(buffer))
319
+ return buffer.length
320
+ })
321
+
322
+ let sent = false
323
+ const reader = {
324
+ Read(p: Uint8Array): [number, typeof io.EOF | null] {
325
+ if (sent) {
326
+ return [0, io.EOF]
327
+ }
328
+ sent = true
329
+ p.set([88, 89, 90], 0)
330
+ return [3, null]
331
+ },
332
+ }
333
+ const file = createHostFile('descriptor-file', 7, { writeSync })
334
+
335
+ const [n, err] = await io.Copy(file, reader)
336
+
337
+ expect(err).toBeNull()
338
+ expect(n).toBe(3)
339
+ expect(Buffer.from(writes).toString('utf8')).toBe('XYZ')
340
+ })
289
341
  })
package/gs/os/meta.json CHANGED
@@ -1,4 +1,8 @@
1
1
  {
2
+ "asyncMethods": {
3
+ "File.ReadFrom": true,
4
+ "File.WriteTo": true
5
+ },
2
6
  "dependencies": [
3
7
  "errors",
4
8
  "internal/byteorder",
@@ -0,0 +1,56 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import * as io from '@goscript/io/index.js'
7
+
8
+ import { Open } from './file_js.gs.js'
9
+
10
+ const tempRoots: string[] = []
11
+
12
+ afterEach(() => {
13
+ for (const root of tempRoots.splice(0)) {
14
+ rmSync(root, { force: true, recursive: true })
15
+ }
16
+ })
17
+
18
+ function makeTempRoot(): string {
19
+ const root = mkdtempSync(join(tmpdir(), 'goscript-os-readdir-'))
20
+ tempRoots.push(root)
21
+ return root
22
+ }
23
+
24
+ describe('os file directory reads', () => {
25
+ it('reads host directory entries through os.File', () => {
26
+ const root = makeTempRoot()
27
+ writeFileSync(join(root, 'a.txt'), 'a')
28
+ writeFileSync(join(root, 'b.txt'), 'b')
29
+
30
+ const [dir, openErr] = Open(root)
31
+ expect(openErr).toBeNull()
32
+ expect(dir).not.toBeNull()
33
+ const [entries, readErr] = dir!.ReadDir(-1)
34
+ expect(readErr).toBeNull()
35
+ expect(entries?.map((entry) => entry!.Name()).sort()).toEqual([
36
+ 'a.txt',
37
+ 'b.txt',
38
+ ])
39
+ expect(dir!.Close()).toBeNull()
40
+ })
41
+
42
+ it('returns EOF after positive-count directory reads are exhausted', () => {
43
+ const root = makeTempRoot()
44
+ writeFileSync(join(root, 'a.txt'), 'a')
45
+
46
+ const [dir] = Open(root)
47
+ const [first, firstErr] = dir!.Readdirnames(1)
48
+ expect(firstErr).toBeNull()
49
+ expect(first).toHaveLength(1)
50
+
51
+ const [second, secondErr] = dir!.Readdirnames(1)
52
+ expect(second).toBeNull()
53
+ expect(secondErr).toBe(io.EOF)
54
+ expect(dir!.Close()).toBeNull()
55
+ })
56
+ })