goscript 0.2.3 → 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 (46) hide show
  1. package/compiler/lowering.go +57 -8
  2. package/compiler/override-registry_test.go +125 -0
  3. package/compiler/skeleton_test.go +49 -2
  4. package/dist/gs/compress/gzip/index.d.ts +41 -0
  5. package/dist/gs/compress/gzip/index.js +235 -0
  6. package/dist/gs/compress/gzip/index.js.map +1 -0
  7. package/dist/gs/io/fs/glob.js +1 -1
  8. package/dist/gs/io/fs/glob.js.map +1 -1
  9. package/dist/gs/io/fs/readlink.d.ts +1 -1
  10. package/dist/gs/io/fs/readlink.js +2 -2
  11. package/dist/gs/io/fs/readlink.js.map +1 -1
  12. package/dist/gs/io/fs/stat.d.ts +4 -2
  13. package/dist/gs/io/fs/stat.js +12 -73
  14. package/dist/gs/io/fs/stat.js.map +1 -1
  15. package/dist/gs/io/fs/sub.d.ts +2 -2
  16. package/dist/gs/io/fs/sub.js +7 -7
  17. package/dist/gs/io/fs/sub.js.map +1 -1
  18. package/dist/gs/io/fs/walk.js +1 -1
  19. package/dist/gs/io/fs/walk.js.map +1 -1
  20. package/dist/gs/net/http/index.d.ts +18 -14
  21. package/dist/gs/net/http/index.js +44 -23
  22. package/dist/gs/net/http/index.js.map +1 -1
  23. package/dist/gs/net/http/pprof/index.d.ts +5 -5
  24. package/dist/gs/net/http/pprof/index.js +21 -21
  25. package/dist/gs/net/http/pprof/index.js.map +1 -1
  26. package/gs/builtin/runtime-contract.test.ts +25 -0
  27. package/gs/compress/gzip/index.test.ts +86 -0
  28. package/gs/compress/gzip/index.ts +297 -0
  29. package/gs/compress/gzip/meta.json +6 -0
  30. package/gs/compress/gzip/parity.json +45 -0
  31. package/gs/embed/index.test.ts +1 -1
  32. package/gs/io/fs/glob.ts +1 -1
  33. package/gs/io/fs/meta.json +3 -0
  34. package/gs/io/fs/readlink.test.ts +2 -2
  35. package/gs/io/fs/readlink.ts +5 -2
  36. package/gs/io/fs/stat.test.ts +79 -0
  37. package/gs/io/fs/stat.ts +24 -10
  38. package/gs/io/fs/sub.test.ts +93 -0
  39. package/gs/io/fs/sub.ts +9 -9
  40. package/gs/io/fs/walk.ts +1 -1
  41. package/gs/net/http/index.test.ts +207 -2
  42. package/gs/net/http/index.ts +68 -37
  43. package/gs/net/http/meta.json +3 -1
  44. package/gs/net/http/pprof/index.test.ts +4 -4
  45. package/gs/net/http/pprof/index.ts +30 -27
  46. package/package.json +1 -1
@@ -0,0 +1,297 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import * as errors from '@goscript/errors/index.js'
3
+ import * as io from '@goscript/io/index.js'
4
+
5
+ type maybeAsyncWriter = {
6
+ Write(p: $.Bytes): [number, $.GoError] | Promise<[number, $.GoError]>
7
+ }
8
+
9
+ type compressionRuntime = {
10
+ gzipSync?: (data: Uint8Array, opts?: Record<string, unknown>) => Uint8Array
11
+ gunzipSync?: (data: Uint8Array) => Uint8Array
12
+ }
13
+
14
+ export const NoCompression = 0
15
+ export const BestSpeed = 1
16
+ export const BestCompression = 9
17
+ export const DefaultCompression = -1
18
+ export const HuffmanOnly = -2
19
+
20
+ export let ErrChecksum = errors.New('gzip: invalid checksum')
21
+ export let ErrHeader = errors.New('gzip: invalid header')
22
+
23
+ export function __goscript_set_ErrChecksum(value: $.GoError): void {
24
+ ErrChecksum = value
25
+ }
26
+
27
+ export function __goscript_set_ErrHeader(value: $.GoError): void {
28
+ ErrHeader = value
29
+ }
30
+
31
+ export class Header {
32
+ Comment = ''
33
+ Extra: $.Bytes = null
34
+ ModTime: any = null
35
+ Name = ''
36
+ OS = 255
37
+ }
38
+
39
+ export class Reader {
40
+ private data: Uint8Array = new Uint8Array(0)
41
+ private offset = 0
42
+ private pending: Promise<{ data: Uint8Array | null; err: $.GoError }> | null =
43
+ null
44
+
45
+ constructor(r?: io.Reader | null) {
46
+ if (r != null) {
47
+ this.Reset(r)
48
+ }
49
+ }
50
+
51
+ async Read(p: $.Bytes): Promise<[number, $.GoError]> {
52
+ const pending = this.pending
53
+ if (pending != null) {
54
+ this.pending = null
55
+ const result = await pending
56
+ if (result.err != null) {
57
+ return [0, result.err]
58
+ }
59
+ this.data = result.data ?? new Uint8Array(0)
60
+ this.offset = 0
61
+ }
62
+ if (this.offset >= this.data.length) {
63
+ return [0, io.EOF]
64
+ }
65
+ const out = $.bytesToUint8Array(p)
66
+ const n = Math.min(out.length, this.data.length - this.offset)
67
+ out.set(this.data.subarray(this.offset, this.offset + n))
68
+ this.offset += n
69
+ return [n, null]
70
+ }
71
+
72
+ Close(): $.GoError {
73
+ return null
74
+ }
75
+
76
+ Reset(r: io.Reader | null): $.GoError {
77
+ if (r == null) {
78
+ return errors.New('gzip: nil reader')
79
+ }
80
+ const result = readGunzipped(r)
81
+ this.data = new Uint8Array(0)
82
+ this.offset = 0
83
+ if (result instanceof Promise) {
84
+ this.pending = result
85
+ return null
86
+ }
87
+ this.pending = null
88
+ if (result.err != null) {
89
+ return result.err
90
+ }
91
+ this.data = result.data ?? new Uint8Array(0)
92
+ return null
93
+ }
94
+ }
95
+
96
+ export class Writer {
97
+ private chunks: Uint8Array[] = []
98
+ private closed = false
99
+
100
+ constructor(
101
+ private w: io.Writer | null,
102
+ private level = DefaultCompression,
103
+ ) {}
104
+
105
+ Write(p: $.Bytes): [number, $.GoError] {
106
+ if (this.closed) {
107
+ return [0, errors.New('gzip: writer closed')]
108
+ }
109
+ const data = $.bytesToUint8Array(p)
110
+ this.chunks.push(data.slice())
111
+ return [data.length, null]
112
+ }
113
+
114
+ async Close(): Promise<$.GoError> {
115
+ if (this.closed) {
116
+ return null
117
+ }
118
+ this.closed = true
119
+ if (this.w == null) {
120
+ return errors.New('gzip: nil writer')
121
+ }
122
+ const compressed = await gzipBytes(concat(this.chunks), this.level)
123
+ const writer = $.pointerValue<maybeAsyncWriter>(this.w)
124
+ const [, err] = await writer.Write(compressed)
125
+ return err
126
+ }
127
+
128
+ Flush(): $.GoError {
129
+ return null
130
+ }
131
+
132
+ Reset(w: io.Writer | null): void {
133
+ this.w = w
134
+ this.chunks = []
135
+ this.closed = false
136
+ }
137
+ }
138
+
139
+ export function NewReader(
140
+ r: io.Reader | null,
141
+ ): [io.ReadCloser | null, $.GoError] {
142
+ const reader = new Reader()
143
+ const err = reader.Reset(r)
144
+ if (err != null) {
145
+ return [null, err]
146
+ }
147
+ return [reader as any, null]
148
+ }
149
+
150
+ export function NewWriter(w: io.Writer | null): Writer {
151
+ return new Writer(w)
152
+ }
153
+
154
+ export function NewWriterLevel(
155
+ w: io.Writer | null,
156
+ level: number,
157
+ ): [Writer | null, $.GoError] {
158
+ if (level < HuffmanOnly || level > BestCompression) {
159
+ return [null, errors.New(`gzip: invalid compression level: ${level}`)]
160
+ }
161
+ return [new Writer(w, level), null]
162
+ }
163
+
164
+ async function gzipBytes(data: Uint8Array, level: number): Promise<Uint8Array> {
165
+ const runtime = nodeCompressionRuntime()
166
+ if (runtime?.gzipSync != null) {
167
+ return runtime.gzipSync(data, { level })
168
+ }
169
+
170
+ const CompressionStreamCtor = (globalThis as any).CompressionStream
171
+ if (typeof CompressionStreamCtor !== 'function') {
172
+ throw new Error('compress/gzip: CompressionStream unavailable')
173
+ }
174
+ return streamTransform(data, new CompressionStreamCtor('gzip'))
175
+ }
176
+
177
+ function gunzipBytes(
178
+ data: Uint8Array,
179
+ ): Uint8Array | Promise<Uint8Array> {
180
+ const runtime = nodeCompressionRuntime()
181
+ if (runtime?.gunzipSync != null) {
182
+ return runtime.gunzipSync(data)
183
+ }
184
+
185
+ const DecompressionStreamCtor = (globalThis as any).DecompressionStream
186
+ if (typeof DecompressionStreamCtor !== 'function') {
187
+ throw new Error('compress/gzip: DecompressionStream unavailable')
188
+ }
189
+ return streamTransform(data, new DecompressionStreamCtor('gzip'))
190
+ }
191
+
192
+ async function streamTransform(
193
+ data: Uint8Array,
194
+ stream: ReadableWritablePair<Uint8Array, Uint8Array>,
195
+ ): Promise<Uint8Array> {
196
+ const body = new ArrayBuffer(data.length)
197
+ new Uint8Array(body).set(data)
198
+ const response = new Response(
199
+ new Blob([body]).stream().pipeThrough(stream as any),
200
+ )
201
+ return new Uint8Array(await response.arrayBuffer())
202
+ }
203
+
204
+ function readGunzipped(
205
+ r: io.Reader,
206
+ ):
207
+ | { data: Uint8Array | null; err: $.GoError }
208
+ | Promise<{ data: Uint8Array | null; err: $.GoError }> {
209
+ const chunks: Uint8Array[] = []
210
+ const buf = $.makeSlice<number>(1, undefined, 'byte')
211
+ while (true) {
212
+ const read = r.Read(buf)
213
+ if (read instanceof Promise) {
214
+ return readGunzippedAsync(read, r, buf, chunks)
215
+ }
216
+ const [n, err] = read
217
+ recordChunk(chunks, buf, n)
218
+ if (err != null) {
219
+ if (err === io.EOF) {
220
+ return inflateRecorded(chunks)
221
+ }
222
+ return { data: null, err }
223
+ }
224
+ }
225
+ }
226
+
227
+ async function readGunzippedAsync(
228
+ read: Promise<[number, $.GoError]>,
229
+ r: io.Reader,
230
+ buf: $.Bytes,
231
+ chunks: Uint8Array[],
232
+ ): Promise<{ data: Uint8Array | null; err: $.GoError }> {
233
+ while (true) {
234
+ const [n, err] = await read
235
+ recordChunk(chunks, buf, n)
236
+ if (err != null) {
237
+ if (err === io.EOF) {
238
+ return inflateRecorded(chunks)
239
+ }
240
+ return { data: null, err }
241
+ }
242
+ read = r.Read(buf) as any
243
+ }
244
+ }
245
+
246
+ function recordChunk(chunks: Uint8Array[], buf: $.Bytes, n: number): void {
247
+ if (n > 0) {
248
+ chunks.push($.bytesToUint8Array($.goSlice(buf, 0, n)).slice())
249
+ }
250
+ }
251
+
252
+ function inflateRecorded(
253
+ chunks: Uint8Array[],
254
+ ): { data: Uint8Array | null; err: $.GoError } | Promise<{
255
+ data: Uint8Array | null
256
+ err: $.GoError
257
+ }> {
258
+ try {
259
+ const inflated = gunzipBytes(concat(chunks))
260
+ if (inflated instanceof Promise) {
261
+ return inflated
262
+ .then((data) => ({ data, err: null }))
263
+ .catch(() => ({ data: null, err: ErrHeader }))
264
+ }
265
+ return { data: inflated, err: null }
266
+ } catch {
267
+ return { data: null, err: ErrHeader }
268
+ }
269
+ }
270
+
271
+ function concat(chunks: Uint8Array[]): Uint8Array {
272
+ let length = 0
273
+ for (const chunk of chunks) {
274
+ length += chunk.length
275
+ }
276
+ const out = new Uint8Array(length)
277
+ let offset = 0
278
+ for (const chunk of chunks) {
279
+ out.set(chunk, offset)
280
+ offset += chunk.length
281
+ }
282
+ return out
283
+ }
284
+
285
+ function nodeCompressionRuntime(): compressionRuntime | null {
286
+ const processObj = (globalThis as any).process
287
+ if (processObj && typeof processObj.getBuiltinModule === 'function') {
288
+ const mod = processObj.getBuiltinModule('zlib')
289
+ if (mod && typeof mod.gzipSync === 'function') {
290
+ return {
291
+ gzipSync: (data, opts) => new Uint8Array(mod.gzipSync(data, opts)),
292
+ gunzipSync: (data) => new Uint8Array(mod.gunzipSync(data)),
293
+ }
294
+ }
295
+ }
296
+ return null
297
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "dependencies": [],
3
+ "asyncMethods": {
4
+ "Writer.Close": true
5
+ }
6
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "strict": true,
4
+ "symbols": {
5
+ "BestCompression": {
6
+ "status": "real"
7
+ },
8
+ "BestSpeed": {
9
+ "status": "real"
10
+ },
11
+ "DefaultCompression": {
12
+ "status": "real"
13
+ },
14
+ "ErrChecksum": {
15
+ "status": "real"
16
+ },
17
+ "ErrHeader": {
18
+ "status": "real"
19
+ },
20
+ "Header": {
21
+ "status": "real"
22
+ },
23
+ "HuffmanOnly": {
24
+ "status": "real"
25
+ },
26
+ "NewReader": {
27
+ "status": "real"
28
+ },
29
+ "NewWriter": {
30
+ "status": "real"
31
+ },
32
+ "NewWriterLevel": {
33
+ "status": "real"
34
+ },
35
+ "NoCompression": {
36
+ "status": "real"
37
+ },
38
+ "Reader": {
39
+ "status": "real"
40
+ },
41
+ "Writer": {
42
+ "status": "real"
43
+ }
44
+ }
45
+ }
@@ -44,7 +44,7 @@ describe('embed.FS', () => {
44
44
  'config-set.bin',
45
45
  ])
46
46
 
47
- const [assetInfo, statErr] = Stat(fsys, 'assets')
47
+ const [assetInfo, statErr] = await Stat(fsys, 'assets')
48
48
  expect(statErr).toBeNull()
49
49
  expect(assetInfo!.IsDir()).toBe(true)
50
50
 
package/gs/io/fs/glob.ts CHANGED
@@ -96,7 +96,7 @@ export async function globWithLimit(
96
96
  }
97
97
  if (!hasMeta(pattern)) {
98
98
  {
99
- const [, statErr] = Stat(fsys, pattern)
99
+ const [, statErr] = await Stat(fsys, pattern)
100
100
  if (statErr != null) {
101
101
  return [null, null]
102
102
  }
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "asyncFunctions": {
3
3
  "Glob": true,
4
+ "Lstat": true,
4
5
  "ReadDir": true,
6
+ "Stat": true,
7
+ "Sub": true,
5
8
  "WalkDir": true
6
9
  }
7
10
  }
@@ -35,8 +35,8 @@ describe('io/fs ReadLink', () => {
35
35
  expect(err?.Error()).toBe('readlink link: invalid argument')
36
36
  })
37
37
 
38
- it('uses Lstat from ReadLinkFS implementations', () => {
39
- const [, err] = Lstat(new linkFS(), 'link')
38
+ it('uses Lstat from ReadLinkFS implementations', async () => {
39
+ const [, err] = await Lstat(new linkFS(), 'link')
40
40
 
41
41
  expect(err).toBe(ErrInvalid)
42
42
  })
@@ -60,10 +60,13 @@ export function ReadLink(fsys: FS, name: string): [string, $.GoError] {
60
60
  return sym!.ReadLink(name)
61
61
  }
62
62
 
63
- export function Lstat(fsys: FS, name: string): [FileInfo, $.GoError] {
63
+ export async function Lstat(
64
+ fsys: FS,
65
+ name: string,
66
+ ): Promise<[FileInfo, $.GoError]> {
64
67
  const { value: sym, ok } = $.typeAssert<ReadLinkFS>(fsys, 'ReadLinkFS')
65
68
  if (!ok) {
66
- return Stat(fsys, name)
69
+ return await Stat(fsys, name)
67
70
  }
68
71
  return sym!.Lstat(name)
69
72
  }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import * as $ from '@goscript/builtin/index.js'
3
+
4
+ import { File, FileInfo, Stat } from './index.js'
5
+
6
+ function fileInfo(name: string, isDir = false): FileInfo {
7
+ return {
8
+ IsDir(): boolean {
9
+ return isDir
10
+ },
11
+ ModTime(): any {
12
+ return null
13
+ },
14
+ Mode(): number {
15
+ return isDir ? 0o040000 : 0
16
+ },
17
+ Name(): string {
18
+ return name
19
+ },
20
+ Size(): number {
21
+ return 0
22
+ },
23
+ Sys(): null {
24
+ return null
25
+ },
26
+ }
27
+ }
28
+
29
+ describe('io/fs Stat override', () => {
30
+ it('awaits async StatFS implementations', async () => {
31
+ const info = fileInfo('pkg', true)
32
+
33
+ const [got, err] = await Stat(
34
+ {
35
+ async Stat(_name: string): Promise<[FileInfo, $.GoError]> {
36
+ return [info, null]
37
+ },
38
+ Open(_name: string): [File, $.GoError] {
39
+ throw new Error('StatFS path should not open')
40
+ },
41
+ },
42
+ 'pkg',
43
+ )
44
+
45
+ expect(err).toBeNull()
46
+ expect(got).toBe(info)
47
+ })
48
+
49
+ it('awaits async Open, File.Stat, and Close fallback implementations', async () => {
50
+ const info = fileInfo('file.txt')
51
+ let closed = false
52
+ const file: File = {
53
+ async Close(): Promise<$.GoError> {
54
+ closed = true
55
+ return null
56
+ },
57
+ Read(_p0: Uint8Array): [number, $.GoError] {
58
+ return [0, null]
59
+ },
60
+ async Stat(): Promise<[FileInfo, $.GoError]> {
61
+ expect(closed).toBe(false)
62
+ return [info, null]
63
+ },
64
+ } as unknown as File
65
+
66
+ const [got, err] = await Stat(
67
+ {
68
+ async Open(_name: string): Promise<[File, $.GoError]> {
69
+ return [file, null]
70
+ },
71
+ } as any,
72
+ 'file.txt',
73
+ )
74
+
75
+ expect(err).toBeNull()
76
+ expect(got).toBe(info)
77
+ expect(closed).toBe(true)
78
+ })
79
+ })
package/gs/io/fs/stat.ts CHANGED
@@ -1,14 +1,25 @@
1
1
  import * as $ from '@goscript/builtin/index.js'
2
- import { FS, FileInfo } from './fs.js'
2
+ import { FS, File, FileInfo } from './fs.js'
3
+
4
+ type maybePromise<T> = T | Promise<T>
3
5
 
4
6
  export type StatFS =
5
7
  | null
6
8
  | ({
7
9
  // Stat returns a FileInfo describing the file.
8
10
  // If there is an error, it should be of type *PathError.
9
- Stat(name: string): [FileInfo, $.GoError]
11
+ Stat(name: string): maybePromise<[FileInfo, $.GoError]>
10
12
  } & FS)
11
13
 
14
+ type asyncFS = null | {
15
+ Open(name: string): maybePromise<[File, $.GoError]>
16
+ }
17
+
18
+ type asyncFile = null | {
19
+ Close(): maybePromise<$.GoError>
20
+ Stat(): maybePromise<[FileInfo, $.GoError]>
21
+ }
22
+
12
23
  $.registerInterfaceType(
13
24
  'StatFS',
14
25
  null, // Zero value for interface is null
@@ -42,21 +53,24 @@ $.registerInterfaceType(
42
53
  //
43
54
  // If fs implements [StatFS], Stat calls fs.Stat.
44
55
  // Otherwise, Stat opens the [File] to stat it.
45
- export function Stat(fsys: FS, name: string): [FileInfo, $.GoError] {
46
- using __defer = new $.DisposableStack()
56
+ export async function Stat(
57
+ fsys: FS,
58
+ name: string,
59
+ ): Promise<[FileInfo, $.GoError]> {
47
60
  {
48
61
  let { value: fsysTyped, ok: ok } = $.typeAssert<StatFS>(fsys, 'StatFS')
49
62
  if (ok) {
50
- return fsysTyped!.Stat(name)
63
+ return await fsysTyped!.Stat(name)
51
64
  }
52
65
  }
53
66
 
54
- let [file, err] = fsys!.Open(name)
67
+ let [file, err] = await (fsys as asyncFS)!.Open(name)
55
68
  if (err != null) {
56
69
  return [null, err]
57
70
  }
58
- __defer.defer(() => {
59
- file!.Close()
60
- })
61
- return file!.Stat()
71
+ try {
72
+ return await (file as asyncFile)!.Stat()
73
+ } finally {
74
+ await (file as asyncFile)!.Close()
75
+ }
62
76
  }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import * as $ from '@goscript/builtin/index.js'
3
+
4
+ import { File, FileInfo, Sub, Stat } from './index.js'
5
+
6
+ function fileInfo(name: string, isDir = false): FileInfo {
7
+ return {
8
+ IsDir(): boolean {
9
+ return isDir
10
+ },
11
+ ModTime(): any {
12
+ return null
13
+ },
14
+ Mode(): number {
15
+ return isDir ? 0o040000 : 0
16
+ },
17
+ Name(): string {
18
+ return name
19
+ },
20
+ Size(): number {
21
+ return 0
22
+ },
23
+ Sys(): null {
24
+ return null
25
+ },
26
+ }
27
+ }
28
+
29
+ describe('io/fs Sub override', () => {
30
+ it('preserves the backing filesystem and directory in fallback subFS', async () => {
31
+ const opened: string[] = []
32
+ const dirInfo = fileInfo('pkg', true)
33
+ const file: File = {
34
+ async Close(): Promise<$.GoError> {
35
+ return null
36
+ },
37
+ Read(_p0: Uint8Array): [number, $.GoError] {
38
+ return [0, null]
39
+ },
40
+ async Stat(): Promise<[FileInfo, $.GoError]> {
41
+ return [dirInfo, null]
42
+ },
43
+ } as unknown as File
44
+
45
+ const [sub, subErr] = await Sub(
46
+ {
47
+ async Open(name: string): Promise<[File, $.GoError]> {
48
+ opened.push(name)
49
+ return [file, null]
50
+ },
51
+ } as any,
52
+ 'pkg',
53
+ )
54
+ expect(subErr).toBeNull()
55
+
56
+ const [got, statErr] = await Stat(sub, '.')
57
+ expect(statErr).toBeNull()
58
+ expect(got).toBe(dirInfo)
59
+ expect(opened).toEqual(['pkg'])
60
+ })
61
+
62
+ it('joins nested fallback subFS directories', async () => {
63
+ const opened: string[] = []
64
+ const fileInfoValue = fileInfo('index.js')
65
+ const file: File = {
66
+ async Close(): Promise<$.GoError> {
67
+ return null
68
+ },
69
+ Read(_p0: Uint8Array): [number, $.GoError] {
70
+ return [0, null]
71
+ },
72
+ async Stat(): Promise<[FileInfo, $.GoError]> {
73
+ return [fileInfoValue, null]
74
+ },
75
+ } as unknown as File
76
+
77
+ const backingFS = {
78
+ async Open(name: string): Promise<[File, $.GoError]> {
79
+ opened.push(name)
80
+ return [file, null]
81
+ },
82
+ } as any
83
+ const [pkgFS, pkgErr] = await Sub(backingFS, '@scope')
84
+ expect(pkgErr).toBeNull()
85
+ const [clientFS, clientErr] = await Sub(pkgFS, 'client')
86
+ expect(clientErr).toBeNull()
87
+
88
+ const [got, statErr] = await Stat(clientFS, 'index.js')
89
+ expect(statErr).toBeNull()
90
+ expect(got).toBe(fileInfoValue)
91
+ expect(opened).toEqual(['@scope/client/index.js'])
92
+ })
93
+ })
package/gs/io/fs/sub.ts CHANGED
@@ -12,7 +12,7 @@ export type SubFS =
12
12
  | null
13
13
  | ({
14
14
  // Sub returns an FS corresponding to the subtree rooted at dir.
15
- Sub(dir: string): [FS, $.GoError]
15
+ Sub(dir: string): [FS, $.GoError] | Promise<[FS, $.GoError]>
16
16
  } & FS)
17
17
 
18
18
  $.registerInterfaceType(
@@ -56,7 +56,7 @@ $.registerInterfaceType(
56
56
  // does not check for symbolic links inside "/prefix" that point to
57
57
  // other directories. That is, [os.DirFS] is not a general substitute for a
58
58
  // chroot-style security mechanism, and Sub does not change that fact.
59
- export function Sub(fsys: FS, dir: string): [FS, $.GoError] {
59
+ export async function Sub(fsys: FS, dir: string): Promise<[FS, $.GoError]> {
60
60
  if (!ValidPath(dir)) {
61
61
  return [null, new PathError({ Err: ErrInvalid, Op: 'sub', Path: dir })]
62
62
  }
@@ -66,10 +66,10 @@ export function Sub(fsys: FS, dir: string): [FS, $.GoError] {
66
66
  {
67
67
  let { value: fsysTyped, ok: ok } = $.typeAssert<SubFS>(fsys, 'SubFS')
68
68
  if (ok) {
69
- return fsysTyped!.Sub(dir)
69
+ return await fsysTyped!.Sub(dir)
70
70
  }
71
71
  }
72
- return [new subFS({}), null]
72
+ return [new subFS({ fsys, dir }) as unknown as FS, null]
73
73
  }
74
74
 
75
75
  class subFS {
@@ -153,14 +153,14 @@ class subFS {
153
153
  return err
154
154
  }
155
155
 
156
- public Open(name: string): [File, $.GoError] {
156
+ public async Open(name: string): Promise<[File, $.GoError]> {
157
157
  const f = this
158
158
  let [full, err] = f!.fullName('open', name)
159
159
  if (err != null) {
160
160
  return [null, err]
161
161
  }
162
162
  let file: File
163
- ;[file, err] = f!.fsys!.Open(full)
163
+ ;[file, err] = await (f!.fsys as any)!.Open(full)
164
164
  return [file, f!.fixErr(err)]
165
165
  }
166
166
 
@@ -225,13 +225,13 @@ class subFS {
225
225
  public Sub(dir: string): [FS, $.GoError] {
226
226
  const f = this
227
227
  if (dir == '.') {
228
- return [f, null]
228
+ return [f as unknown as FS, null]
229
229
  }
230
- let [_full, err] = f!.fullName('sub', dir)
230
+ let [full, err] = f!.fullName('sub', dir)
231
231
  if (err != null) {
232
232
  return [null, err]
233
233
  }
234
- return [new subFS({}), null]
234
+ return [new subFS({ fsys: f.fsys, dir: full }) as unknown as FS, null]
235
235
  }
236
236
 
237
237
  // Register this type with the runtime type system