Skip to content

Commit

Permalink
Add js.Batch
Browse files Browse the repository at this point in the history
Fixes #12626
Closes #7499
Closes #12874
  • Loading branch information
bep committed Sep 24, 2024
1 parent e07028c commit f3e1915
Show file tree
Hide file tree
Showing 17 changed files with 1,441 additions and 163 deletions.
15 changes: 15 additions & 0 deletions common/herrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ func IsNotExist(err error) bool {
return false
}

// IsExist returns true if the error is a file exists error.
// Unlike os.IsExist, this also considers wrapped errors.
func IsExist(err error) bool {
if os.IsExist(err) {
return true
}

// os.IsExist does not consider wrapped errors.
if os.IsExist(errors.Unwrap(err)) {
return true
}

return false
}

var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)

const deferredPrefix = "__hdeferred/"
Expand Down
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
6 changes: 4 additions & 2 deletions hugolib/integrationtest_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,9 @@ func (s *IntegrationTestBuilder) negate(match string) (string, bool) {
func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...string) {
s.Helper()
content := strings.TrimSpace(s.FileContent(filename))

for _, m := range matches {
cm := qt.Commentf("File: %s Match %s", filename, m)
cm := qt.Commentf("File: %s Match %s\nContent:\n%s", filename, m, content)
lines := strings.Split(m, "\n")
for _, match := range lines {
match = strings.TrimSpace(match)
Expand All @@ -291,7 +292,8 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
var negate bool
match, negate = s.negate(match)
if negate {
s.Assert(content, qt.Not(qt.Contains), match, cm)
if !s.Assert(content, qt.Not(qt.Contains), match, cm) {
}
continue
}
s.Assert(content, qt.Contains, match, cm)
Expand Down
12 changes: 8 additions & 4 deletions media/mediaType.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,13 @@ func (t Types) GetByType(tp string) (Type, bool) {
return Type{}, false
}

func (t Types) normalizeSuffix(s string) string {
return strings.ToLower(strings.TrimPrefix(s, "."))
}

// BySuffix will return all media types matching a suffix.
func (t Types) BySuffix(suffix string) []Type {
suffix = strings.ToLower(suffix)
suffix = t.normalizeSuffix(suffix)
var types []Type
for _, tt := range t {
if tt.hasSuffix(suffix) {
Expand All @@ -287,7 +291,7 @@ func (t Types) BySuffix(suffix string) []Type {

// GetFirstBySuffix will return the first type matching the given suffix.
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
suffix = strings.ToLower(suffix)
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
return tt, SuffixInfo{
Expand All @@ -304,7 +308,7 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
// is ambiguous.
// The lookup is case insensitive.
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
suffix = strings.ToLower(suffix)
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
if found {
Expand All @@ -324,7 +328,7 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
}

func (t Types) IsTextSuffix(suffix string) bool {
suffix = strings.ToLower(suffix)
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
return tt.IsText()
Expand Down
6 changes: 6 additions & 0 deletions resources/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var (
_ resource.Cloner = (*genericResource)(nil)
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
_ resource.Identifier = (*genericResource)(nil)
_ resource.PathProvider = (*genericResource)(nil)
_ identity.IdentityGroupProvider = (*genericResource)(nil)
_ identity.DependencyManagerProvider = (*genericResource)(nil)
_ identity.Identity = (*genericResource)(nil)
Expand Down Expand Up @@ -463,6 +464,11 @@ func (l *genericResource) Key() string {
return key
}

// TODO1 test and document this. Consider adding it to the Resource interface.
func (l *genericResource) Path() string {
return l.paths.TargetPath()
}

func (l *genericResource) MediaType() media.Type {
return l.sd.MediaType
}
Expand Down
7 changes: 7 additions & 0 deletions resources/resource/resourcetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ type MediaTypeProvider interface {
MediaType() media.Type
}

type PathProvider interface {
// Path is the relative path to this resource.
// In most cases this will be the same as the RelPermalink(),
// but it will not trigger any lazy publishing.
Path() string
}

type ResourceLinksProvider interface {
// Permalink represents the absolute link to this resource.
Permalink() string
Expand Down
131 changes: 63 additions & 68 deletions resources/resource_transformers/js/build.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -16,25 +16,20 @@ package js
import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/spf13/afero"

"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/media"

"github.com/evanw/esbuild/pkg/api"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/text"

"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/identity"
"github.com/spf13/afero"

"github.com/evanw/esbuild/pkg/api"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
)
Expand All @@ -53,54 +48,55 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
}
}

type buildTransformation struct {
optsm map[string]any
c *Client
// 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},
)
}

func (t *buildTransformation) Key() internal.ResourceTransformationKey {
return internal.NewResourceTransformationKey("jsbuild", t.optsm)
// 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},
)
}

func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
ctx.OutMediaType = media.Builtin.JavascriptType
func (c *Client) BuildBundle(opts Options) (api.BuildResult, error) {
return c.build(opts, nil)
}

opts, err := decodeOptions(t.optsm)
if err != nil {
return err
// Note that transformCtx may be nil.
func (c *Client) build(opts Options, transformCtx *resources.ResourceTransformationCtx) (api.BuildResult, error) {
dependencyManager := opts.DependencyManager
if transformCtx != nil {
dependencyManager = transformCtx.DependencyManager // TODO1
}

if opts.TargetPath != "" {
ctx.OutPath = opts.TargetPath
} else {
ctx.ReplaceOutPathExtension(".js")
if dependencyManager == nil {
dependencyManager = identity.NopManager
}

src, err := io.ReadAll(ctx.From)
if err != nil {
return err
}
opts.ResolveDir = c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
opts.TsConfig = c.rs.ResolveJSConfigFile("tsconfig.json")

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")
if err := opts.validate(); err != nil {
return api.BuildResult{}, err
}

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

buildOptions.Plugins, err = createBuildPlugins(ctx.DependencyManager, t.c, opts)
buildOptions.Plugins, err = createBuildPlugins(c, dependencyManager, opts)
if err != nil {
return err
return api.BuildResult{}, err
}

if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
buildOptions.Outdir, err = os.MkdirTemp(os.TempDir(), "compileOutput")
if err != nil {
return err
return api.BuildResult{}, err
}
defer os.Remove(buildOptions.Outdir)
}
Expand All @@ -110,13 +106,13 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
for i, ext := range opts.Inject {
impPath := filepath.FromSlash(ext)
if filepath.IsAbs(impPath) {
return fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
return api.BuildResult{}, fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
}

m := resolveComponentInAssets(t.c.rs.Assets.Fs, impPath)
m := resolveComponentInAssets(c.rs.Assets.Fs, impPath)

if m == nil {
return fmt.Errorf("inject: file %q not found", ext)
return api.BuildResult{}, fmt.Errorf("inject: file %q not found", ext)
}

opts.Inject[i] = m.Filename
Expand All @@ -138,7 +134,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
}
path := loc.File
if path == stdinImporter {
path = ctx.SourcePath
path = transformCtx.SourcePath
}

errorMessage := msg.Text
Expand All @@ -154,7 +150,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
f, err = hugofs.Os.Open(path)
} else {
var fi os.FileInfo
fi, err = t.c.sfs.Fs.Stat(path)
fi, err = c.sfs.Fs.Stat(path)
if err == nil {
m := fi.(hugofs.FileMetaInfo).Meta()
path = m.Filename
Expand Down Expand Up @@ -185,38 +181,37 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
// Return 1, log the rest.
for i, err := range errors {
if i > 0 {
t.c.rs.Logger.Errorf("js.Build failed: %s", err)
c.rs.Logger.Errorf("js.Build failed: %s", err)
}
}

return errors[0]
return result, errors[0]
}

if buildOptions.Sourcemap == api.SourceMapExternal {
content := string(result.OutputFiles[1].Contents)
symPath := path.Base(ctx.OutPath) + ".map"
re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
// TODO1 option etc. fmt.Printf("%s", api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{}))

if transformCtx != nil {
if buildOptions.Sourcemap == api.SourceMapExternal {
content := string(result.OutputFiles[1].Contents)
symPath := path.Base(transformCtx.OutPath) + ".map"
re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")

if err = transformCtx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
return result, err
}
_, err := transformCtx.To.Write([]byte(content))
if err != nil {
return result, err
}
} else {
_, err := transformCtx.To.Write(result.OutputFiles[0].Contents)
if err != nil {
return result, err
}

if err = ctx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
return err
}
_, err := ctx.To.Write([]byte(content))
if err != nil {
return err
}
} else {
_, err := ctx.To.Write(result.OutputFiles[0].Contents)
if err != nil {
return err
}
}
return nil
}

// Process process esbuild transform
func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
return res.Transform(
&buildTransformation{c: c, optsm: opts},
)
return result, nil
}
Loading

0 comments on commit f3e1915

Please sign in to comment.