SandpointsGitHook/vendor/github.com/bep/godartsass/transpiler.go
2021-03-20 23:21:23 +01:00

464 lines
11 KiB
Go

// Package godartsass provides a Go API for the Dass Sass Embedded protocol.
//
// Use the Start function to create and start a new thread safe transpiler.
// Close it when done.
package godartsass
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"path"
"strings"
"time"
"os"
"os/exec"
"sync"
"github.com/cli/safeexec"
"github.com/bep/godartsass/internal/embeddedsass"
"google.golang.org/protobuf/proto"
)
const defaultDartSassEmbeddedFilename = "dart-sass-embedded"
// ErrShutdown will be returned from Execute and Close if the transpiler is or
// is about to be shut down.
var ErrShutdown = errors.New("connection is shut down")
// Start creates and starts a new SCSS transpiler that communicates with the
// Dass Sass Embedded protocol via Stdin and Stdout.
//
// Closing the transpiler will shut down the process.
//
// Note that the Transpiler is thread safe, and the recommended way of using
// this is to create one and use that for all the SCSS processing needed.
func Start(opts Options) (*Transpiler, error) {
if err := opts.init(); err != nil {
return nil, err
}
// See https://github.com/golang/go/issues/38736
bin, err := safeexec.LookPath(opts.DartSassEmbeddedFilename)
if err != nil {
return nil, err
}
cmd := exec.Command(bin)
cmd.Stderr = os.Stderr
conn, err := newConn(cmd)
if err != nil {
return nil, err
}
if err := conn.Start(); err != nil {
return nil, err
}
t := &Transpiler{
opts: opts,
conn: conn,
lenBuf: make([]byte, binary.MaxVarintLen64),
pending: make(map[uint32]*call),
}
go t.input()
return t, nil
}
// Transpiler controls transpiling of SCSS into CSS.
type Transpiler struct {
opts Options
// stdin/stdout of the Dart Sass protocol
conn byteReadWriteCloser
lenBuf []byte
msgBuf []byte
closing bool
shutdown bool
// Protects the sending of messages to Dart Sass.
sendMu sync.Mutex
mu sync.Mutex // Protects all below.
seq uint32
pending map[uint32]*call
}
// Result holds the result returned from Execute.
type Result struct {
CSS string
SourceMap string
}
// SassError is the error returned from Execute on compile errors.
type SassError struct {
Message string `json:"message"`
Span struct {
Text string `json:"text"`
Start struct {
Offset int `json:"offset"`
Column int `json:"column"`
} `json:"start"`
End struct {
Offset int `json:"offset"`
Column int `json:"column"`
} `json:"end"`
Url string `json:"url"`
Context string `json:"context"`
} `json:"span"`
}
func (e SassError) Error() string {
span := e.Span
file := path.Clean(strings.TrimPrefix(span.Url, "file:"))
return fmt.Sprintf("file: %q, context: %q: %s", file, span.Context, e.Message)
}
// Close closes the stream to the embedded Dart Sass Protocol, shutting it down.
// If it is already shutting down, ErrShutdown is returned.
func (t *Transpiler) Close() error {
t.sendMu.Lock()
defer t.sendMu.Unlock()
t.mu.Lock()
defer t.mu.Unlock()
if t.closing {
return ErrShutdown
}
t.closing = true
err := t.conn.Close()
return err
}
// Execute transpiles the string Source given in Args into CSS.
// If Dart Sass resturns a "compile failure", the error returned will be
// of type SassError.
func (t *Transpiler) Execute(args Args) (Result, error) {
var result Result
createInboundMessage := func(seq uint32) (*embeddedsass.InboundMessage, error) {
if err := args.init(seq, t.opts); err != nil {
return nil, err
}
message := &embeddedsass.InboundMessage_CompileRequest_{
CompileRequest: &embeddedsass.InboundMessage_CompileRequest{
Importers: args.sassImporters,
Style: args.sassOutputStyle,
Input: &embeddedsass.InboundMessage_CompileRequest_String_{
String_: &embeddedsass.InboundMessage_CompileRequest_StringInput{
Syntax: args.sassSourceSyntax,
Source: args.Source,
Url: args.URL,
},
},
SourceMap: args.EnableSourceMap,
},
}
return &embeddedsass.InboundMessage{
Message: message,
}, nil
}
call, err := t.newCall(createInboundMessage, args)
if err != nil {
return result, err
}
select {
case call = <-call.Done:
case <-time.After(t.opts.Timeout):
return result, errors.New("timeout waiting for Dart Sass to respond; if you're running with Embedded Sass protocol < beta6, you need to upgrade")
}
if call.Error != nil {
return result, call.Error
}
response := call.Response
csp := response.Message.(*embeddedsass.OutboundMessage_CompileResponse_)
switch resp := csp.CompileResponse.Result.(type) {
case *embeddedsass.OutboundMessage_CompileResponse_Success:
result.CSS = resp.Success.Css
result.SourceMap = resp.Success.SourceMap
case *embeddedsass.OutboundMessage_CompileResponse_Failure:
asJson, err := json.Marshal(resp.Failure)
if err != nil {
return result, err
}
var sassErr SassError
err = json.Unmarshal(asJson, &sassErr)
if err != nil {
return result, err
}
return result, sassErr
default:
return result, fmt.Errorf("unsupported response type: %T", resp)
}
return result, nil
}
func (t *Transpiler) getCall(id uint32) *call {
t.mu.Lock()
defer t.mu.Unlock()
call, found := t.pending[id]
if !found {
panic(fmt.Sprintf("call with ID %d not found", id))
}
return call
}
func (t *Transpiler) input() {
var err error
for err == nil {
// The header is the length in bytes of the remaining message.
var l uint64
l, err = binary.ReadUvarint(t.conn)
if err != nil {
break
}
plen := int(l)
if len(t.msgBuf) < plen {
t.msgBuf = make([]byte, plen)
}
buf := t.msgBuf[:plen]
_, err = io.ReadFull(t.conn, buf)
if err != nil {
break
}
var msg embeddedsass.OutboundMessage
if err = proto.Unmarshal(buf, &msg); err != nil {
break
}
switch c := msg.Message.(type) {
case *embeddedsass.OutboundMessage_CompileResponse_:
id := c.CompileResponse.Id
// Attach it to the correct pending call.
t.mu.Lock()
call := t.pending[id]
delete(t.pending, id)
t.mu.Unlock()
if call == nil {
err = fmt.Errorf("call with ID %d not found", id)
break
}
call.Response = &msg
call.done()
case *embeddedsass.OutboundMessage_CanonicalizeRequest_:
call := t.getCall(c.CanonicalizeRequest.CompilationId)
resolved, resolveErr := call.importResolver.CanonicalizeURL(c.CanonicalizeRequest.GetUrl())
var response *embeddedsass.InboundMessage_CanonicalizeResponse
if resolveErr != nil {
response = &embeddedsass.InboundMessage_CanonicalizeResponse{
Id: c.CanonicalizeRequest.GetId(),
Result: &embeddedsass.InboundMessage_CanonicalizeResponse_Error{
Error: resolveErr.Error(),
},
}
} else {
var url *embeddedsass.InboundMessage_CanonicalizeResponse_Url
if resolved != "" {
url = &embeddedsass.InboundMessage_CanonicalizeResponse_Url{
Url: resolved,
}
}
response = &embeddedsass.InboundMessage_CanonicalizeResponse{
Id: c.CanonicalizeRequest.GetId(),
Result: url,
}
}
err = t.sendInboundMessage(
&embeddedsass.InboundMessage{
Message: &embeddedsass.InboundMessage_CanonicalizeResponse_{
CanonicalizeResponse: response,
},
},
)
case *embeddedsass.OutboundMessage_ImportRequest_:
call := t.getCall(c.ImportRequest.CompilationId)
url := c.ImportRequest.GetUrl()
contents, loadErr := call.importResolver.Load(url)
var response *embeddedsass.InboundMessage_ImportResponse
var sourceMapURL string
// Dart Sass expect a browser-accessible URL or an empty string.
// If no URL is supplied, a `data:` URL wil be generated
// automatically from `contents`
if hasScheme(url) {
sourceMapURL = url
}
if loadErr != nil {
response = &embeddedsass.InboundMessage_ImportResponse{
Id: c.ImportRequest.GetId(),
Result: &embeddedsass.InboundMessage_ImportResponse_Error{
Error: loadErr.Error(),
},
}
} else {
response = &embeddedsass.InboundMessage_ImportResponse{
Id: c.ImportRequest.GetId(),
Result: &embeddedsass.InboundMessage_ImportResponse_Success{
Success: &embeddedsass.InboundMessage_ImportResponse_ImportSuccess{
Contents: contents,
SourceMapUrl: sourceMapURL,
},
},
}
}
err = t.sendInboundMessage(
&embeddedsass.InboundMessage{
Message: &embeddedsass.InboundMessage_ImportResponse_{
ImportResponse: response,
},
},
)
case *embeddedsass.OutboundMessage_LogEvent_:
// Drop these for now.
case *embeddedsass.OutboundMessage_Error:
err = fmt.Errorf("SASS error: %s", c.Error.GetMessage())
default:
err = fmt.Errorf("unsupported response message type. %T", msg.Message)
}
}
// Terminate pending calls.
t.sendMu.Lock()
defer t.sendMu.Unlock()
t.mu.Lock()
defer t.mu.Unlock()
t.shutdown = true
isEOF := err == io.EOF || strings.Contains(err.Error(), "already closed")
if isEOF {
if t.closing {
err = ErrShutdown
} else {
err = io.ErrUnexpectedEOF
}
}
for _, call := range t.pending {
call.Error = err
call.done()
}
}
func (t *Transpiler) newCall(createInbound func(seq uint32) (*embeddedsass.InboundMessage, error), args Args) (*call, error) {
t.mu.Lock()
id := t.seq
req, err := createInbound(id)
if err != nil {
t.mu.Unlock()
return nil, err
}
call := &call{
Request: req,
Done: make(chan *call, 1),
importResolver: args.ImportResolver,
}
if t.shutdown || t.closing {
t.mu.Unlock()
call.Error = ErrShutdown
call.done()
return call, nil
}
t.pending[id] = call
t.seq++
t.mu.Unlock()
switch c := call.Request.Message.(type) {
case *embeddedsass.InboundMessage_CompileRequest_:
c.CompileRequest.Id = id
default:
return nil, fmt.Errorf("unsupported request message type. %T", call.Request.Message)
}
return call, t.sendInboundMessage(call.Request)
}
func (t *Transpiler) sendInboundMessage(message *embeddedsass.InboundMessage) error {
t.sendMu.Lock()
defer t.sendMu.Unlock()
t.mu.Lock()
if t.closing || t.shutdown {
t.mu.Unlock()
return ErrShutdown
}
t.mu.Unlock()
out, err := proto.Marshal(message)
if err != nil {
return fmt.Errorf("failed to marshal request: %s", err)
}
// Every message must begin with a varint indicating the length in bytes of
// the remaining message.
reqLen := uint64(len(out))
n := binary.PutUvarint(t.lenBuf, reqLen)
_, err = t.conn.Write(t.lenBuf[:n])
if err != nil {
return err
}
n, err = t.conn.Write(out)
if n != len(out) {
return errors.New("failed to write payload")
}
return err
}
type call struct {
Request *embeddedsass.InboundMessage
Response *embeddedsass.OutboundMessage
importResolver ImportResolver
Error error
Done chan *call
}
func (call *call) done() {
select {
case call.Done <- call:
default:
}
}
func hasScheme(s string) bool {
u, err := url.ParseRequestURI(s)
if err != nil {
return false
}
return u.Scheme != ""
}