add a plugin api (#111)

This commit is contained in:
Evan Wallace
2020-07-09 17:04:53 -07:00
parent e3334171b2
commit 38c5be5b53
16 changed files with 1879 additions and 103 deletions

View File

@@ -102,7 +102,8 @@ const (
type Loader uint8
const (
LoaderJS Loader = iota
LoaderNone Loader = iota
LoaderJS
LoaderJSX
LoaderTS
LoaderTSX
@@ -113,6 +114,7 @@ const (
LoaderFile
LoaderBinary
LoaderCSS
LoaderDefault
)
type Platform uint8
@@ -148,11 +150,12 @@ type Engine struct {
}
type Location struct {
File string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
type Message struct {
@@ -230,6 +233,7 @@ type BuildOptions struct {
EntryPoints []string
Stdin *StdinOptions
Write bool
Plugins []Plugin
}
type StdinOptions struct {
@@ -297,3 +301,60 @@ type TransformResult struct {
func Transform(input string, options TransformOptions) TransformResult {
return transformImpl(input, options)
}
////////////////////////////////////////////////////////////////////////////////
// Plugin API
type Plugin struct {
Name string
Setup func(PluginBuild)
}
type PluginBuild interface {
OnResolve(options OnResolveOptions, callback func(OnResolveArgs) (OnResolveResult, error))
OnLoad(options OnLoadOptions, callback func(OnLoadArgs) (OnLoadResult, error))
}
type OnResolveOptions struct {
Filter string
Namespace string
}
type OnResolveArgs struct {
Path string
Importer string
Namespace string
ResolveDir string
}
type OnResolveResult struct {
PluginName string
Errors []Message
Warnings []Message
Path string
External bool
Namespace string
}
type OnLoadOptions struct {
Filter string
Namespace string
}
type OnLoadArgs struct {
Path string
Namespace string
}
type OnLoadResult struct {
PluginName string
Errors []Message
Warnings []Message
Contents *string
ResolveDir string
Loader Loader
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@@ -103,6 +104,8 @@ func validateASCIIOnly(value Charset) bool {
func validateLoader(value Loader) config.Loader {
switch value {
case LoaderNone:
return config.LoaderNone
case LoaderJS:
return config.LoaderJS
case LoaderJSX:
@@ -125,6 +128,8 @@ func validateLoader(value Loader) config.Loader {
return config.LoaderBinary
case LoaderCSS:
return config.LoaderCSS
case LoaderDefault:
return config.LoaderDefault
default:
panic("Invalid loader")
}
@@ -386,20 +391,21 @@ func validateOutputExtensions(log logger.Log, outExtensions map[string]string) m
return result
}
func messagesOfKind(kind logger.MsgKind, msgs []logger.Msg) []Message {
func convertMessagesToPublic(kind logger.MsgKind, msgs []logger.Msg) []Message {
var filtered []Message
for _, msg := range msgs {
if msg.Kind == kind {
var location *Location
loc := msg.Location
if msg.Location != nil {
loc := msg.Location
if loc != nil {
location = &Location{
File: loc.File,
Line: loc.Line,
Column: loc.Column,
Length: loc.Length,
LineText: loc.LineText,
File: loc.File,
Namespace: loc.Namespace,
Line: loc.Line,
Column: loc.Column,
Length: loc.Length,
LineText: loc.LineText,
}
}
@@ -412,6 +418,35 @@ func messagesOfKind(kind logger.MsgKind, msgs []logger.Msg) []Message {
return filtered
}
func convertMessagesToInternal(msgs []logger.Msg, kind logger.MsgKind, messages []Message) []logger.Msg {
for _, message := range messages {
var location *logger.MsgLocation
loc := message.Location
if loc != nil {
namespace := loc.Namespace
if namespace == "" {
namespace = "file"
}
location = &logger.MsgLocation{
File: loc.File,
Namespace: namespace,
Line: loc.Line,
Column: loc.Column,
Length: loc.Length,
LineText: loc.LineText,
}
}
msgs = append(msgs, logger.Msg{
Kind: kind,
Text: message.Text,
Location: location,
})
}
return msgs
}
////////////////////////////////////////////////////////////////////////////////
// Build API
@@ -538,6 +573,8 @@ func buildImpl(buildOpts BuildOptions) BuildResult {
log.AddError(nil, logger.Loc{}, "Splitting currently only works with the \"esm\" format")
}
loadPlugins(&options, realFS, log, buildOpts.Plugins)
var outputFiles []OutputFile
// Stop now if there were errors
@@ -608,8 +645,8 @@ func buildImpl(buildOpts BuildOptions) BuildResult {
msgs := log.Done()
return BuildResult{
Errors: messagesOfKind(logger.Error, msgs),
Warnings: messagesOfKind(logger.Warning, msgs),
Errors: convertMessagesToPublic(logger.Error, msgs),
Warnings: convertMessagesToPublic(logger.Warning, msgs),
OutputFiles: outputFiles,
}
}
@@ -656,6 +693,14 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
}
}
// Apply default values
if transformOpts.Sourcefile == "" {
transformOpts.Sourcefile = "<stdin>"
}
if transformOpts.Loader == LoaderNone {
transformOpts.Loader = LoaderJS
}
// Convert and validate the transformOpts
jsFeatures, cssFeatures := validateFeatures(log, transformOpts.Target, transformOpts.Engines)
options := config.Options{
@@ -728,9 +773,145 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
msgs := log.Done()
return TransformResult{
Errors: messagesOfKind(logger.Error, msgs),
Warnings: messagesOfKind(logger.Warning, msgs),
Errors: convertMessagesToPublic(logger.Error, msgs),
Warnings: convertMessagesToPublic(logger.Warning, msgs),
Code: code,
Map: sourceMap,
}
}
////////////////////////////////////////////////////////////////////////////////
// Plugin API
type pluginImpl struct {
log logger.Log
fs fs.FS
plugin config.Plugin
}
func (impl *pluginImpl) OnResolve(options OnResolveOptions, callback func(OnResolveArgs) (OnResolveResult, error)) {
filter, err := config.CompileFilterForPlugin(impl.plugin.Name, "OnResolve", options.Filter)
if filter == nil {
impl.log.AddError(nil, logger.Loc{}, err.Error())
return
}
impl.plugin.OnResolve = append(impl.plugin.OnResolve, config.OnResolve{
Name: impl.plugin.Name,
Filter: filter,
Namespace: options.Namespace,
Callback: func(args config.OnResolveArgs) (result config.OnResolveResult) {
response, err := callback(OnResolveArgs{
Path: args.Path,
Importer: args.Importer.Text,
Namespace: args.Importer.Namespace,
ResolveDir: args.ResolveDir,
})
result.PluginName = response.PluginName
if err != nil {
result.ThrownError = err
return
}
if response.Namespace == "" {
response.Namespace = "file"
}
result.Path = logger.Path{Text: response.Path, Namespace: response.Namespace}
result.External = response.External
// Convert log messages
if len(response.Errors)+len(response.Warnings) > 0 {
msgs := make(sortableMsgs, 0, len(response.Errors)+len(response.Warnings))
msgs = convertMessagesToInternal(msgs, logger.Error, response.Errors)
msgs = convertMessagesToInternal(msgs, logger.Warning, response.Warnings)
sort.Sort(msgs)
result.Msgs = msgs
}
return
},
})
}
func (impl *pluginImpl) OnLoad(options OnLoadOptions, callback func(OnLoadArgs) (OnLoadResult, error)) {
filter, err := config.CompileFilterForPlugin(impl.plugin.Name, "OnLoad", options.Filter)
if filter == nil {
impl.log.AddError(nil, logger.Loc{}, err.Error())
return
}
impl.plugin.OnLoad = append(impl.plugin.OnLoad, config.OnLoad{
Filter: filter,
Namespace: options.Namespace,
Callback: func(args config.OnLoadArgs) (result config.OnLoadResult) {
response, err := callback(OnLoadArgs{
Path: args.Path.Text,
Namespace: args.Path.Namespace,
})
result.PluginName = response.PluginName
if err != nil {
result.ThrownError = err
return
}
result.Contents = response.Contents
result.Loader = validateLoader(response.Loader)
if absPath := validatePath(impl.log, impl.fs, response.ResolveDir); absPath != "" {
result.AbsResolveDir = absPath
}
// Convert log messages
if len(response.Errors)+len(response.Warnings) > 0 {
msgs := make(sortableMsgs, 0, len(response.Errors)+len(response.Warnings))
msgs = convertMessagesToInternal(msgs, logger.Error, response.Errors)
msgs = convertMessagesToInternal(msgs, logger.Warning, response.Warnings)
sort.Sort(msgs)
result.Msgs = msgs
}
return
},
})
}
// This type is just so we can use Go's native sort function
type sortableMsgs []logger.Msg
func (a sortableMsgs) Len() int { return len(a) }
func (a sortableMsgs) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a sortableMsgs) Less(i int, j int) bool {
ai := a[i].Location
aj := a[j].Location
if ai == nil || aj == nil {
return ai == nil && aj != nil
}
if ai.File != aj.File {
return ai.File < aj.File
}
if ai.Line != aj.Line {
return ai.Line < aj.Line
}
if ai.Column != aj.Column {
return ai.Column < aj.Column
}
return a[i].Text < a[j].Text
}
func loadPlugins(options *config.Options, fs fs.FS, log logger.Log, plugins []Plugin) {
for i, item := range plugins {
if item.Name == "" {
log.AddError(nil, logger.Loc{}, fmt.Sprintf("Plugin at index %d is missing a name", i))
continue
}
impl := &pluginImpl{
fs: fs,
log: log,
plugin: config.Plugin{Name: item.Name},
}
item.Setup(impl)
options.Plugins = append(options.Plugins, impl.plugin)
}
}

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/pkg/api"
)
@@ -188,7 +189,7 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt
return fmt.Errorf("Missing \"=\": %q", value)
}
ext, text := value[:equals], value[equals+1:]
loader, err := parseLoader(text)
loader, err := helpers.ParseLoader(text)
if err != nil {
return err
}
@@ -196,7 +197,7 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt
case strings.HasPrefix(arg, "--loader="):
value := arg[len("--loader="):]
loader, err := parseLoader(value)
loader, err := helpers.ParseLoader(value)
if err != nil {
return err
}
@@ -419,36 +420,6 @@ outer:
return
}
func parseLoader(text string) (api.Loader, error) {
switch text {
case "js":
return api.LoaderJS, nil
case "jsx":
return api.LoaderJSX, nil
case "ts":
return api.LoaderTS, nil
case "tsx":
return api.LoaderTSX, nil
case "css":
return api.LoaderCSS, nil
case "json":
return api.LoaderJSON, nil
case "text":
return api.LoaderText, nil
case "base64":
return api.LoaderBase64, nil
case "dataurl":
return api.LoaderDataURL, nil
case "file":
return api.LoaderFile, nil
case "binary":
return api.LoaderBinary, nil
default:
return 0, fmt.Errorf("Invalid loader: %q (valid: "+
"js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary)", text)
}
}
// This returns either BuildOptions, TransformOptions, or an error
func parseOptionsForRun(osArgs []string) (*api.BuildOptions, *api.TransformOptions, error) {
// If there's an entry point or we're bundling, then we're building