Skip to content

Commit 025bb40

Browse files
committed
fix: println, os capability detection and binding
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent c706510 commit 025bb40

9 files changed

Lines changed: 743 additions & 211 deletions

File tree

gs/builtin/builtin.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import type { Slice, SliceProxy } from './slice.js'
2+
import { writeHostStdoutText } from './hostio.js'
3+
import { formatPrintedArgs } from './print.js'
24
import { isSliceProxy } from './slice.js'
35

46
/**
57
* Implementation of Go's built-in println function
68
* @param args Arguments to print
79
*/
810
export function println(...args: any[]): void {
9-
if (args.length === 0) {
10-
// Bun's console.log() with no args doesn't print a newline, so we explicitly print an empty string
11-
console.log('')
12-
} else {
13-
console.log(...args)
14-
}
11+
const message = (args.length === 0 ? '' : formatPrintedArgs(args)) + '\n'
12+
writeHostStdoutText(message)
1513
}
1614

1715
/**

gs/builtin/hostio.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
3+
import {
4+
resetHostRuntimeForTests,
5+
writeHostStderrText,
6+
writeHostStdoutText,
7+
} from './hostio.js'
8+
9+
const originalDeno = (globalThis as any).Deno
10+
const originalProcess = (globalThis as any).process
11+
12+
afterEach(() => {
13+
if (originalDeno === undefined) {
14+
delete (globalThis as any).Deno
15+
} else {
16+
;(globalThis as any).Deno = originalDeno
17+
}
18+
19+
if (originalProcess === undefined) {
20+
delete (globalThis as any).process
21+
} else {
22+
;(globalThis as any).process = originalProcess
23+
}
24+
25+
resetHostRuntimeForTests()
26+
})
27+
28+
describe('hostio text writes', () => {
29+
it('uses sync node fs writes for stdout and stderr', () => {
30+
const writes: Array<{ fd: number; bytes: number[] }> = []
31+
const writeSync = vi.fn(
32+
(
33+
fd: number,
34+
buffer: Uint8Array,
35+
_offset?: number,
36+
length?: number,
37+
_position?: number | null,
38+
) => {
39+
writes.push({
40+
bytes: Array.from(buffer.subarray(0, length ?? buffer.length)),
41+
fd,
42+
})
43+
return length ?? buffer.length
44+
},
45+
)
46+
47+
delete (globalThis as any).Deno
48+
;(globalThis as any).process = {
49+
getBuiltinModule: vi.fn(() => ({
50+
readSync: vi.fn(),
51+
writeSync,
52+
})),
53+
stderr: { write: vi.fn() },
54+
stdout: { write: vi.fn() },
55+
}
56+
resetHostRuntimeForTests()
57+
58+
writeHostStdoutText('ok\n')
59+
writeHostStderrText('err\n')
60+
61+
expect(writeSync).toHaveBeenCalledTimes(2)
62+
expect(writes).toEqual([
63+
{ bytes: [111, 107, 10], fd: 1 },
64+
{ bytes: [101, 114, 114, 10], fd: 2 },
65+
])
66+
expect((globalThis as any).process.stdout.write).not.toHaveBeenCalled()
67+
expect((globalThis as any).process.stderr.write).not.toHaveBeenCalled()
68+
})
69+
})

gs/builtin/hostio.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
export class HostUnsupportedError extends Error {
2+
constructor() {
3+
super('operation not implemented in JavaScript environment')
4+
}
5+
}
6+
7+
export type NodeFSModule = {
8+
readSync(
9+
fd: number,
10+
buffer: Uint8Array,
11+
offset?: number,
12+
length?: number,
13+
position?: number | null,
14+
): number
15+
writeSync(
16+
fd: number,
17+
buffer: Uint8Array,
18+
offset?: number,
19+
length?: number,
20+
position?: number | null,
21+
): number
22+
closeSync?(fd: number): void
23+
fstatSync?(fd: number): any
24+
fsyncSync?(fd: number): void
25+
ftruncateSync?(fd: number, len?: number): void
26+
openSync?(path: string, flags: number | string, mode?: number): number
27+
chmodSync?(path: string, mode: number): void
28+
chownSync?(path: string, uid: number, gid: number): void
29+
lchownSync?(path: string, uid: number, gid: number): void
30+
linkSync?(existingPath: string, newPath: string): void
31+
lstatSync?(path: string): any
32+
mkdirSync?(path: string, options?: number | { mode?: number; recursive?: boolean }): void
33+
readFileSync?(path: string): Uint8Array
34+
readdirSync?(path: string, options?: { withFileTypes?: boolean }): any[]
35+
readlinkSync?(path: string): string
36+
renameSync?(oldPath: string, newPath: string): void
37+
rmSync?(path: string, options?: { force?: boolean; recursive?: boolean }): void
38+
rmdirSync?(path: string): void
39+
statSync?(path: string): any
40+
symlinkSync?(target: string, path: string): void
41+
truncateSync?(path: string, len?: number): void
42+
unlinkSync?(path: string): void
43+
utimesSync?(path: string, atime: Date | number, mtime: Date | number): void
44+
writeFileSync?(path: string, data: Uint8Array, options?: { mode?: number }): void
45+
}
46+
47+
export type DenoStream = {
48+
readSync?(buffer: Uint8Array): number | null
49+
writeSync?(buffer: Uint8Array): number
50+
}
51+
52+
export type DenoFileLike = DenoStream & {
53+
close?(): void
54+
rid?: number
55+
seekSync?(offset: number, whence: number): number
56+
syncSync?(): void
57+
statSync?(): any
58+
truncateSync?(len?: number): void
59+
}
60+
61+
type HostReadFD = (fd: number, buffer: Uint8Array) => number | null
62+
type HostWriteFD = (fd: number, buffer: Uint8Array) => number
63+
type HostTextWrite = (data: string) => void
64+
65+
export type HostRuntime = {
66+
deno: any | null
67+
nodeFS: NodeFSModule | null
68+
platform: string
69+
processObj: any | null
70+
getEnv(name: string): string
71+
getStdioHandle(fd: number): DenoFileLike | null
72+
readFD: HostReadFD
73+
writeFD: HostWriteFD
74+
writeStderrText: HostTextWrite
75+
writeStdoutText: HostTextWrite
76+
}
77+
78+
const encoder = new TextEncoder()
79+
80+
function getDynamicRequire(): ((specifier: string) => unknown) | null {
81+
try {
82+
return Function(
83+
"return typeof require !== 'undefined' ? require : null",
84+
)() as ((specifier: string) => unknown) | null
85+
} catch {
86+
return null
87+
}
88+
}
89+
90+
function writeAllSync(
91+
writeChunk: (chunk: Uint8Array) => number,
92+
buffer: Uint8Array,
93+
): number {
94+
let offset = 0
95+
while (offset < buffer.length) {
96+
const n = writeChunk(buffer.subarray(offset))
97+
if (!Number.isFinite(n) || n < 0) {
98+
throw new Error(`invalid write result: ${n}`)
99+
}
100+
if (n === 0) {
101+
throw new Error('short write')
102+
}
103+
offset += n
104+
}
105+
return buffer.length
106+
}
107+
108+
function writeAllText(
109+
writeChunk: (chunk: Uint8Array) => number,
110+
data: string,
111+
): void {
112+
const bytes = encoder.encode(data)
113+
if (bytes.length === 0) {
114+
return
115+
}
116+
writeAllSync(writeChunk, bytes)
117+
}
118+
119+
function detectNodeFS(processObj: any | null): NodeFSModule | null {
120+
if (processObj && typeof processObj.getBuiltinModule === 'function') {
121+
const module = processObj.getBuiltinModule('fs')
122+
if (
123+
module &&
124+
typeof module.readSync === 'function' &&
125+
typeof module.writeSync === 'function'
126+
) {
127+
return module as NodeFSModule
128+
}
129+
}
130+
131+
const requireFn = getDynamicRequire()
132+
if (requireFn) {
133+
for (const specifier of ['node:fs', 'fs']) {
134+
try {
135+
const module = requireFn(specifier) as NodeFSModule | null
136+
if (
137+
module &&
138+
typeof module.readSync === 'function' &&
139+
typeof module.writeSync === 'function'
140+
) {
141+
return module
142+
}
143+
} catch {
144+
// Try the next fallback.
145+
}
146+
}
147+
}
148+
149+
return null
150+
}
151+
152+
function unsupportedReadFD(_fd: number, _buffer: Uint8Array): number | null {
153+
throw new HostUnsupportedError()
154+
}
155+
156+
function unsupportedWriteFD(_fd: number, _buffer: Uint8Array): number {
157+
throw new HostUnsupportedError()
158+
}
159+
160+
function fallbackConsoleWriter(method: 'error' | 'log'): HostTextWrite {
161+
return (data: string) => {
162+
const consoleMethod = (globalThis as any).console?.[method]
163+
if (!consoleMethod) {
164+
return
165+
}
166+
if (data.endsWith('\n')) {
167+
consoleMethod.call((globalThis as any).console, data.slice(0, -1))
168+
return
169+
}
170+
consoleMethod.call((globalThis as any).console, data)
171+
}
172+
}
173+
174+
function detectHostRuntime(): HostRuntime {
175+
const globalObj = globalThis as any
176+
const deno = globalObj.Deno ?? null
177+
const processObj = globalObj.process ?? null
178+
const nodeFS = detectNodeFS(processObj)
179+
180+
const getStdioHandle = (fd: number): DenoFileLike | null => {
181+
if (!deno) {
182+
return null
183+
}
184+
switch (fd) {
185+
case 0:
186+
return deno.stdin ?? null
187+
case 1:
188+
return deno.stdout ?? null
189+
case 2:
190+
return deno.stderr ?? null
191+
default:
192+
return null
193+
}
194+
}
195+
196+
const platform =
197+
deno?.build?.os ??
198+
processObj?.platform ??
199+
'unknown'
200+
201+
const getEnv = (name: string): string => {
202+
if (deno?.env?.get) {
203+
try {
204+
return deno.env.get(name) ?? ''
205+
} catch {
206+
return ''
207+
}
208+
}
209+
return processObj?.env?.[name] ?? ''
210+
}
211+
212+
let readFD: HostReadFD = unsupportedReadFD
213+
let writeFD: HostWriteFD = unsupportedWriteFD
214+
let writeStdoutText: HostTextWrite = fallbackConsoleWriter('log')
215+
let writeStderrText: HostTextWrite = fallbackConsoleWriter('error')
216+
217+
if (deno) {
218+
readFD = (fd: number, buffer: Uint8Array): number | null => {
219+
const handle = getStdioHandle(fd)
220+
if (!handle || typeof handle.readSync !== 'function') {
221+
throw new HostUnsupportedError()
222+
}
223+
return handle.readSync(buffer)
224+
}
225+
writeFD = (fd: number, buffer: Uint8Array): number => {
226+
const handle = getStdioHandle(fd)
227+
if (!handle || typeof handle.writeSync !== 'function') {
228+
throw new HostUnsupportedError()
229+
}
230+
return writeAllSync(
231+
(chunk: Uint8Array) => handle.writeSync!(chunk),
232+
buffer,
233+
)
234+
}
235+
writeStdoutText = (data: string) => {
236+
const handle = getStdioHandle(1)
237+
if (!handle || typeof handle.writeSync !== 'function') {
238+
fallbackConsoleWriter('log')(data)
239+
return
240+
}
241+
writeAllText((chunk: Uint8Array) => handle.writeSync!(chunk), data)
242+
}
243+
writeStderrText = (data: string) => {
244+
const handle = getStdioHandle(2)
245+
if (!handle || typeof handle.writeSync !== 'function') {
246+
fallbackConsoleWriter('error')(data)
247+
return
248+
}
249+
writeAllText((chunk: Uint8Array) => handle.writeSync!(chunk), data)
250+
}
251+
} else if (nodeFS) {
252+
readFD = (fd: number, buffer: Uint8Array): number | null =>
253+
nodeFS.readSync(fd, buffer, 0, buffer.length, null)
254+
writeFD = (fd: number, buffer: Uint8Array): number =>
255+
writeAllSync(
256+
(chunk: Uint8Array) =>
257+
nodeFS.writeSync(fd, chunk, 0, chunk.length, null),
258+
buffer,
259+
)
260+
writeStdoutText = (data: string) =>
261+
writeAllText(
262+
(chunk: Uint8Array) => nodeFS.writeSync(1, chunk, 0, chunk.length, null),
263+
data,
264+
)
265+
writeStderrText = (data: string) =>
266+
writeAllText(
267+
(chunk: Uint8Array) => nodeFS.writeSync(2, chunk, 0, chunk.length, null),
268+
data,
269+
)
270+
}
271+
272+
return {
273+
deno,
274+
getEnv,
275+
getStdioHandle,
276+
nodeFS,
277+
platform,
278+
processObj,
279+
readFD,
280+
writeFD,
281+
writeStderrText,
282+
writeStdoutText,
283+
}
284+
}
285+
286+
export let hostRuntime = detectHostRuntime()
287+
288+
export function resetHostRuntimeForTests(): void {
289+
hostRuntime = detectHostRuntime()
290+
}
291+
292+
export function writeHostStdoutText(data: string): void {
293+
hostRuntime.writeStdoutText(data)
294+
}
295+
296+
export function writeHostStderrText(data: string): void {
297+
hostRuntime.writeStderrText(data)
298+
}

0 commit comments

Comments
 (0)