package api import ( "context" "fmt" "io" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/vfs/osvfs" ) // StdioServerOptions configures the STDIO-based API server. type StdioServerOptions struct { In io.ReadCloser Out io.WriteCloser Err io.Writer Cwd string DefaultLibraryPath string // PipePath, if set, listens on a named pipe (Windows) or Unix domain // socket instead of using In/Out for communication. PipePath string // Callbacks specifies which filesystem operations should be delegated // to the client (e.g., "readFile", "fileExists"). Empty means no callbacks. Callbacks []string // Async enables JSON-RPC protocol with async connection handling. // When false (default), uses MessagePack protocol with sync connection. Async bool // CollectTiming enables per-request server processing-time measurement. // When enabled, the server accumulates each request's processing time into // running totals and a recent-request ring buffer. Response messages are // left unchanged; the client folds this data into its own timing snapshot // on demand via getServerTiming / resetServerTiming requests. CollectTiming bool } // StdioServer runs an API session over STDIO using MessagePack protocol. // This is the entry point for the synchronous STDIO-based API used by // native TypeScript tooling integration. type StdioServer struct { options *StdioServerOptions } // NewStdioServer creates a new STDIO-based API server. func NewStdioServer(options *StdioServerOptions) *StdioServer { if options.Cwd == "" { panic("StdioServerOptions.Cwd is required") } return &StdioServer{ options: options, } } // Run starts the server and blocks until the connection closes. func (s *StdioServer) Run(ctx context.Context) error { var transport Transport if s.options.PipePath != "" { t, err := NewPipeTransport(s.options.PipePath) if err != nil { return fmt.Errorf("failed to create pipe transport: %w", err) } defer t.Close() transport = t } else { t := NewStdioTransport(s.options.In, s.options.Out) defer t.Close() transport = t } fs := bundled.WrapFS(osvfs.FS()) // Wrap the base FS with callbackFS if callbacks are requested var callbackFS *callbackFS if len(s.options.Callbacks) > 0 { callbackFS = newCallbackFS(fs, s.options.Callbacks) fs = callbackFS } projectSession := project.NewSession(&project.SessionInit{ BackgroundCtx: ctx, Logger: nil, // TODO: Add logging support FS: fs, Options: &project.SessionOptions{ CurrentDirectory: s.options.Cwd, DefaultLibraryPath: s.options.DefaultLibraryPath, PositionEncoding: lsproto.PositionEncodingKindUTF8, LoggingEnabled: false, }, }) session := NewSession(projectSession, &SessionOptions{ UseBinaryResponses: !s.options.Async, // Only msgpack uses binary responses }) defer session.Close() // Accept connection from transport rwc, err := transport.Accept() if err != nil { return fmt.Errorf("failed to accept connection: %w", err) } // Create protocol and connection based on async mode var conn Conn if s.options.Async { protocol := NewJSONRPCProtocol(rwc) asyncConn := NewAsyncConnWithProtocol(rwc, protocol, session) asyncConn.SetCollectTiming(s.options.CollectTiming) conn = asyncConn } else { protocol := NewMessagePackProtocol(rwc) syncConn := NewSyncConn(rwc, protocol, session) syncConn.SetCollectTiming(s.options.CollectTiming) conn = syncConn } // If callbacks are enabled, set the connection on the FS if callbackFS != nil { callbackFS.SetConnection(ctx, conn) } return conn.Run(ctx) }