Skip to content

Commit

Permalink
Add js.Batch
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Jul 2, 2024
1 parent 7b6dafc commit 0779824
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 80 deletions.
14 changes: 14 additions & 0 deletions common/maps/scratch.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ func (c *Scratch) Get(key string) any {
return val
}

// GetOrCreate returns the value for the given key if it exists, or creates it
// using the given func and stores that value in the map.
// For internal use.
func (c *Scratch) GetOrCreate(key string, create func() any) any {
c.mu.Lock()
defer c.mu.Unlock()
if val, found := c.values[key]; found {
return val
}
val := create()
c.values[key] = val
return val
}

// Values returns the raw backing map. Note that you should just use
// this method on the locally scoped Scratch instances you obtain via newScratch, not
// .Page.Scratch etc., as that will lead to concurrency issues.
Expand Down
58 changes: 39 additions & 19 deletions resources/resource_transformers/js/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,32 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {

type buildTransformation struct {
optsm map[string]any
opts Options
c *Client
}

func (t *buildTransformation) Key() internal.ResourceTransformationKey {
return internal.NewResourceTransformationKey("jsbuild", t.optsm)
// Pick the most stable key source.
var v any = t.optsm
if v == nil {
v = t.opts
}
return internal.NewResourceTransformationKey("jsbuild", v)
}

func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
ctx.OutMediaType = media.Builtin.JavascriptType

opts, err := decodeOptions(t.optsm)
if err != nil {
return err
if t.optsm != nil {
optsExt, err := decodeOptions(t.optsm)
if err != nil {
return err
}
t.opts.ExternalOptions = optsExt
}

if opts.TargetPath != "" {
ctx.OutPath = opts.TargetPath
if t.opts.TargetPath != "" {
ctx.OutPath = t.opts.TargetPath
} else {
ctx.ReplaceOutPathExtension(".js")
}
Expand All @@ -81,18 +90,22 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
return err
}

opts.sourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
opts.resolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
opts.contents = string(src)
opts.mediaType = ctx.InMediaType
opts.tsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")
t.opts.SourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
t.opts.ResolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
t.opts.Contents = string(src)
t.opts.MediaType = ctx.InMediaType
t.opts.TsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")

buildOptions, err := toBuildOptions(opts)
if err := t.opts.validate(); err != nil {
return err
}

buildOptions, err := toBuildOptions(t.opts)
if err != nil {
return err
}

buildOptions.Plugins, err = createBuildPlugins(ctx.DependencyManager, t.c, opts)
buildOptions.Plugins, err = t.createBuildPlugins(ctx.DependencyManager)
if err != nil {
return err
}
Expand All @@ -105,9 +118,9 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
defer os.Remove(buildOptions.Outdir)
}

if opts.Inject != nil {
if t.opts.Inject != nil {
// Resolve the absolute filenames.
for i, ext := range opts.Inject {
for i, ext := range t.opts.Inject {
impPath := filepath.FromSlash(ext)
if filepath.IsAbs(impPath) {
return fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
Expand All @@ -119,11 +132,11 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
return fmt.Errorf("inject: file %q not found", ext)
}

opts.Inject[i] = m.Filename
t.opts.Inject[i] = m.Filename

}

buildOptions.Inject = opts.Inject
buildOptions.Inject = t.opts.Inject

}

Expand Down Expand Up @@ -214,9 +227,16 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
return nil
}

// Process process esbuild transform
func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
// ProcessExernal processes a resource with the user provided options.
func (c *Client) ProcessExernal(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
return res.Transform(
&buildTransformation{c: c, optsm: opts},
)
}

// ProcessExernal processes a resource with the given options.
func (c *Client) ProcessInternal(res resources.ResourceTransformer, opts Options) (resource.Resource, error) {
return res.Transform(
&buildTransformation{c: c, opts: opts},
)
}
93 changes: 65 additions & 28 deletions resources/resource_transformers/js/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,35 @@ const (
stdinImporter = "<stdin>"
)

// Options esbuild configuration
type Options struct {
ExternalOptions
InternalOptions
}

func (opts *Options) validate() error {
if opts.ImportOnResolveFunc != nil && opts.ImportOnLoadFunc == nil {
return fmt.Errorf("ImportOnLoadFunc must be set if ImportOnResolveFunc is set")
}
if opts.ImportOnResolveFunc == nil && opts.ImportOnLoadFunc != nil {
return fmt.Errorf("ImportOnResolveFunc must be set if ImportOnLoadFunc is set")
}
return nil
}

// InternalOptions holds internal options for the js.Build template function.
type InternalOptions struct {
MediaType media.Type
OutDir string
Contents string
SourceDir string
ResolveDir string
TsConfig string
ImportOnResolveFunc func(string) string
ImportOnLoadFunc func(string) string
}

// ExternalOptions holds user facing options for the js.Build template function.
type ExternalOptions struct {
// If not set, the source path will be used as the base target path.
// Note that the target path's extension may change if the target MIME type
// is different, e.g. when the source is TypeScript.
Expand Down Expand Up @@ -105,17 +132,10 @@ type Options struct {
// Deprecated: This no longer have any effect and will be removed.
// TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba
AvoidTDZ bool

mediaType media.Type
outDir string
contents string
sourceDir string
resolveDir string
tsConfig string
}

func decodeOptions(m map[string]any) (Options, error) {
var opts Options
func decodeOptions(m map[string]any) (ExternalOptions, error) {
var opts ExternalOptions

if err := mapstructure.WeakDecode(m, &opts); err != nil {
return opts, err
Expand Down Expand Up @@ -212,17 +232,24 @@ func resolveComponentInAssets(fs afero.Fs, impPath string) *hugofs.FileMeta {
return m
}

func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) ([]api.Plugin, error) {
fs := c.rs.Assets
func (t *buildTransformation) createBuildPlugins(depsManager identity.Manager) ([]api.Plugin, error) {
fs := t.c.rs.Assets

resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
impPath := args.Path
if opts.Shims != nil {
override, found := opts.Shims[impPath]
if t.opts.Shims != nil {
override, found := t.opts.Shims[impPath]
if found {
impPath = override
}
}

if t.opts.ImportOnResolveFunc != nil {
if s := t.opts.ImportOnResolveFunc(impPath); s != "" {
return api.OnResolveResult{Path: s, Namespace: nsImportHugo}, nil
}
}

isStdin := args.Importer == stdinImporter
var relDir string
if !isStdin {
Expand All @@ -236,7 +263,7 @@ func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) (

relDir = filepath.Dir(rel)
} else {
relDir = opts.sourceDir
relDir = t.opts.SourceDir
}

// Imports not starting with a "." is assumed to live relative to /assets.
Expand All @@ -255,7 +282,7 @@ func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) (
// This should be a small number of elements, and when
// in server mode, we may get stale entries on renames etc.,
// but that shouldn't matter too much.
c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
t.c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
return api.OnResolveResult{Path: m.Filename, Namespace: nsImportHugo}, nil
}

Expand All @@ -272,24 +299,34 @@ func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) (
})
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
b, err := os.ReadFile(args.Path)
if err != nil {
return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
var c string
if t.opts.ImportOnLoadFunc != nil {
if s := t.opts.ImportOnLoadFunc(args.Path); s != "" {
c = s
}
}

if c == "" {
b, err := os.ReadFile(args.Path)
if err != nil {
return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
}
c = string(b)
}
c := string(b)

return api.OnLoadResult{
// See https://github.com/evanw/esbuild/issues/502
// This allows all modules to resolve dependencies
// in the main project's node_modules.
ResolveDir: opts.resolveDir,
ResolveDir: t.opts.ResolveDir,
Contents: &c,
Loader: loaderFromFilename(args.Path),
}, nil
})
},
}

params := opts.Params
params := t.opts.Params
if params == nil {
// This way @params will always resolve to something.
params = make(map[string]any)
Expand Down Expand Up @@ -353,7 +390,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
return
}

mediaType := opts.mediaType
mediaType := opts.MediaType
if mediaType.IsZero() {
mediaType = media.Builtin.JavascriptType
}
Expand All @@ -371,7 +408,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
case media.Builtin.JSXType.SubType:
loader = api.LoaderJSX
default:
err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
err = fmt.Errorf("unsupported Media Type: %q", opts.MediaType)
return
}

Expand Down Expand Up @@ -408,7 +445,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
}

// By default we only need to specify outDir and no outFile
outDir := opts.outDir
outDir := opts.OutDir
outFile := ""
var sourceMap api.SourceMap
switch opts.SourceMap {
Expand Down Expand Up @@ -446,14 +483,14 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
JSX: jsx,
JSXImportSource: opts.JSXImportSource,

Tsconfig: opts.tsConfig,
Tsconfig: opts.TsConfig,

// Note: We're not passing Sourcefile to ESBuild.
// This makes ESBuild pass `stdin` as the Importer to the import
// resolver, which is what we need/expect.
Stdin: &api.StdinOptions{
Contents: opts.contents,
ResolveDir: opts.resolveDir,
Contents: opts.Contents,
ResolveDir: opts.ResolveDir,
Loader: loader,
},
}
Expand Down
Loading

0 comments on commit 0779824

Please sign in to comment.