From 9e649533386eee7cba99d8375d215325555ec7e6 Mon Sep 17 00:00:00 2001 From: Alexandre Vaz Date: Wed, 13 May 2026 12:15:14 -0300 Subject: [PATCH] js: Return error for missing batch imports A stale or removed resource used by js.Batch could panic while the esbuild import loader read its content during a rebuild. Return the read error through the loader so esbuild reports a normal build error and a later rebuild can recover when the file returns. Closes #13737 --- internal/js/esbuild/batch.go | 11 +++++----- internal/js/esbuild/batch_integration_test.go | 20 +++++++++++++++++++ internal/js/esbuild/options.go | 2 +- internal/js/esbuild/resolve.go | 5 ++++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go index 28fc11ffa..61c6b6d5c 100644 --- a/internal/js/esbuild/batch.go +++ b/internal/js/esbuild/batch.go @@ -45,7 +45,6 @@ import ( "github.com/gohugoio/hugo/resources/resource_factories/create" "github.com/gohugoio/hugo/tpl/tplimpl" "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" ) var _ js.Batcher = (*batcher)(nil) @@ -567,17 +566,17 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) { } return "" }, - ImportOnLoadFunc: func(args api.OnLoadArgs) string { + ImportOnLoadFunc: func(args api.OnLoadArgs) (string, error) { imp := args.Path if r, found := state.importResource.Get(imp); found { - content, err := r.(resource.ContentProvider).Content(ctx) + content, err := resources.InternalResourceSourceContent(ctx, r) if err != nil { - panic(err) + return "", fmt.Errorf("failed to read import %q: %w", resources.InternalResourceSourcePathBestEffort(r), err) } - return cast.ToString(content) + return content, nil } - return "" + return "", nil }, ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage { if importContext, found := state.importerImportContext.Get(args.Path); found { diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go index e29eaf9c1..16309985c 100644 --- a/internal/js/esbuild/batch_integration_test.go +++ b/internal/js/esbuild/batch_integration_test.go @@ -261,6 +261,26 @@ func TestBatchRenameBundledScript(t *testing.T) { b.Build() } +func TestBatchMissingImportedAssetIssue13737(t *testing.T) { + files := jsBatchFilesTemplate + b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) + b.AssertFileContent("public/mybatch/mygroup.js", "Hello, Main") + + filename := filepath.Join(b.Cfg.WorkingDir, "assets/js/main.js") + content, err := os.ReadFile(filename) + b.Assert(err, qt.IsNil) + b.Assert(os.Remove(filename), qt.IsNil) + + _, err = b.BuildE() + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, "failed to read import") + b.Assert(err.Error(), qt.Contains, "main.js") + + b.Assert(os.WriteFile(filename, content, 0o666), qt.IsNil) + b.Build() + b.AssertFileContent("public/mybatch/mygroup.js", "Hello, Main") +} + func TestBatchErrorScriptResourceNotSet(t *testing.T) { files := strings.Replace(jsBatchFilesTemplate, `(resources.Get "js/main.js")`, `(resources.Get "js/doesnotexist.js")`, 1) b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) diff --git a/internal/js/esbuild/options.go b/internal/js/esbuild/options.go index f5112f4cd..11d14c3db 100644 --- a/internal/js/esbuild/options.go +++ b/internal/js/esbuild/options.go @@ -248,7 +248,7 @@ type InternalOptions struct { TsConfig string EntryPoints []string ImportOnResolveFunc func(string, api.OnResolveArgs) string - ImportOnLoadFunc func(api.OnLoadArgs) string + ImportOnLoadFunc func(api.OnLoadArgs) (string, error) ImportParamsOnLoadFunc func(args api.OnLoadArgs) json.RawMessage ErrorMessageResolveFunc func(api.Message) *ErrorMessageResolved ResolveSourceMapSource func(string) string // Used to resolve paths in error source maps. diff --git a/internal/js/esbuild/resolve.go b/internal/js/esbuild/resolve.go index 9d4cffe68..3438af375 100644 --- a/internal/js/esbuild/resolve.go +++ b/internal/js/esbuild/resolve.go @@ -314,7 +314,10 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana }) build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: NsHugoImportResolveFunc}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { - c := opts.ImportOnLoadFunc(args) + c, err := opts.ImportOnLoadFunc(args) + if err != nil { + return api.OnLoadResult{}, err + } if c == "" { return api.OnLoadResult{}, fmt.Errorf("ImportOnLoadFunc failed to resolve %q", args.Path) }