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.
- package/cmd/goscript/cmd_compile.go +28 -8
- package/cmd/goscript/cmd_compile_test.go +105 -6
- package/compiler/build-flags.go +9 -10
- package/compiler/gotest/runner_test.go +127 -0
- package/compiler/lowered-program.go +1 -0
- package/compiler/lowering.go +1325 -194
- package/compiler/lowering_bench_test.go +350 -0
- package/compiler/override-registry_test.go +43 -0
- package/compiler/package-graph.go +61 -4
- package/compiler/package-graph_test.go +30 -0
- package/compiler/semantic-model-types.go +8 -0
- package/compiler/semantic-model.go +447 -22
- package/compiler/semantic-model_test.go +138 -0
- package/compiler/skeleton_test.go +1436 -50
- package/compiler/typescript-emitter.go +47 -4
- package/dist/gs/builtin/builtin.d.ts +2 -2
- package/dist/gs/builtin/builtin.js +20 -0
- package/dist/gs/builtin/builtin.js.map +1 -1
- package/dist/gs/builtin/channel.js +36 -9
- package/dist/gs/builtin/channel.js.map +1 -1
- package/dist/gs/builtin/slice.js +5 -0
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/builtin/type.d.ts +1 -1
- package/dist/gs/builtin/type.js +80 -8
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/bytes/bytes.gs.d.ts +7 -5
- package/dist/gs/bytes/bytes.gs.js +10 -4
- package/dist/gs/bytes/bytes.gs.js.map +1 -1
- package/dist/gs/compress/zlib/index.d.ts +3 -3
- package/dist/gs/compress/zlib/index.js +88 -26
- package/dist/gs/compress/zlib/index.js.map +1 -1
- package/dist/gs/crypto/sha1/index.d.ts +5 -0
- package/dist/gs/crypto/sha1/index.js +103 -0
- package/dist/gs/crypto/sha1/index.js.map +1 -0
- package/dist/gs/crypto/sha256/index.js +2 -5
- package/dist/gs/crypto/sha256/index.js.map +1 -1
- package/dist/gs/crypto/sha512/index.js +2 -5
- package/dist/gs/crypto/sha512/index.js.map +1 -1
- package/dist/gs/embed/index.d.ts +6 -0
- package/dist/gs/embed/index.js +210 -5
- package/dist/gs/embed/index.js.map +1 -1
- package/dist/gs/fmt/fmt.d.ts +4 -4
- package/dist/gs/fmt/fmt.js +93 -19
- package/dist/gs/fmt/fmt.js.map +1 -1
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
- package/dist/gs/io/fs/readdir.js +5 -3
- package/dist/gs/io/fs/readdir.js.map +1 -1
- package/dist/gs/io/io.d.ts +18 -11
- package/dist/gs/io/io.js +107 -44
- package/dist/gs/io/io.js.map +1 -1
- package/dist/gs/math/bits/index.d.ts +26 -5
- package/dist/gs/math/bits/index.js +13 -24
- package/dist/gs/math/bits/index.js.map +1 -1
- package/dist/gs/net/http/httptest/index.js +7 -5
- package/dist/gs/net/http/httptest/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +11 -1
- package/dist/gs/net/http/index.js +157 -11
- package/dist/gs/net/http/index.js.map +1 -1
- package/dist/gs/os/types_js.gs.d.ts +6 -2
- package/dist/gs/os/types_js.gs.js +169 -8
- package/dist/gs/os/types_js.gs.js.map +1 -1
- package/dist/gs/os/zero_copy_posix.gs.js +1 -1
- package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
- package/dist/gs/reflect/type.d.ts +1 -0
- package/dist/gs/reflect/type.js +80 -51
- package/dist/gs/reflect/type.js.map +1 -1
- package/dist/gs/strings/reader.d.ts +1 -1
- package/dist/gs/strings/reader.js +2 -2
- package/dist/gs/strings/reader.js.map +1 -1
- package/dist/gs/sync/sync.d.ts +2 -1
- package/dist/gs/sync/sync.js +37 -16
- package/dist/gs/sync/sync.js.map +1 -1
- package/dist/gs/syscall/js/index.js +9 -0
- package/dist/gs/syscall/js/index.js.map +1 -1
- package/dist/gs/testing/testing.js +8 -6
- package/dist/gs/testing/testing.js.map +1 -1
- package/gs/builtin/builtin.ts +25 -2
- package/gs/builtin/channel.ts +47 -9
- package/gs/builtin/runtime-contract.test.ts +78 -0
- package/gs/builtin/slice.ts +7 -0
- package/gs/builtin/type.ts +97 -8
- package/gs/bytes/bytes.gs.ts +19 -10
- package/gs/bytes/bytes.test.ts +17 -0
- package/gs/compress/zlib/index.test.ts +97 -0
- package/gs/compress/zlib/index.ts +117 -27
- package/gs/compress/zlib/meta.json +4 -1
- package/gs/context/context.test.ts +5 -1
- package/gs/crypto/sha1/index.test.ts +45 -0
- package/gs/crypto/sha1/index.ts +127 -0
- package/gs/crypto/sha1/meta.json +8 -0
- package/gs/crypto/sha256/index.test.ts +14 -2
- package/gs/crypto/sha256/index.ts +3 -6
- package/gs/crypto/sha512/index.test.ts +17 -2
- package/gs/crypto/sha512/index.ts +3 -6
- package/gs/embed/index.test.ts +87 -0
- package/gs/embed/index.ts +229 -5
- package/gs/fmt/fmt.test.ts +61 -3
- package/gs/fmt/fmt.ts +115 -22
- package/gs/fmt/meta.json +6 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
- package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
- package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
- package/gs/io/fs/readdir.test.ts +38 -0
- package/gs/io/fs/readdir.ts +7 -3
- package/gs/io/io.test.ts +135 -0
- package/gs/io/io.ts +143 -63
- package/gs/io/meta.json +7 -1
- package/gs/math/bits/index.ts +52 -28
- package/gs/net/http/httptest/index.test.ts +34 -2
- package/gs/net/http/httptest/index.ts +23 -8
- package/gs/net/http/index.test.ts +46 -0
- package/gs/net/http/index.ts +178 -12
- package/gs/os/file_unix_js.test.ts +52 -0
- package/gs/os/meta.json +4 -0
- package/gs/os/readdir.test.ts +56 -0
- package/gs/os/types_js.gs.ts +169 -8
- package/gs/os/zero_copy_posix.gs.ts +1 -2
- package/gs/reflect/deepequal.test.ts +10 -1
- package/gs/reflect/type.ts +91 -56
- package/gs/reflect/typefor.test.ts +31 -1
- package/gs/strings/meta.json +5 -2
- package/gs/strings/reader.test.ts +2 -2
- package/gs/strings/reader.ts +2 -2
- package/gs/sync/meta.json +2 -0
- package/gs/sync/sync.test.ts +41 -1
- package/gs/sync/sync.ts +41 -16
- package/gs/syscall/js/index.test.ts +18 -0
- package/gs/syscall/js/index.ts +12 -0
- package/gs/testing/testing.test.ts +32 -3
- package/gs/testing/testing.ts +13 -10
- 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 {
|
|
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).
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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',
|
package/gs/net/http/index.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
224
|
-
public async RoundTrip(
|
|
225
|
-
|
|
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
|
|
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
|
@@ -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
|
+
})
|