mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
e51e761d9c
When CSS imports assets via the file loader (fonts, images), the emitted URLs were relative to the CSS output directory. That broke when the CSS was inlined into HTML, since browsers then resolved the URLs against the page rather than the CSS file. Set esbuild's PublicPath to the CSS output directory joined with the site base path so URLs work whether the CSS is published as a file or inlined. Fixes #14849
162 lines
5.3 KiB
Go
162 lines
5.3 KiB
Go
// 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.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package js
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/pkg/api"
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/hugolib/filesystems"
|
|
"github.com/gohugoio/hugo/internal/js/esbuild"
|
|
|
|
"github.com/gohugoio/hugo/resources"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
)
|
|
|
|
// Client context for ESBuild.
|
|
type Client struct {
|
|
c *esbuild.BuildClient
|
|
}
|
|
|
|
// New creates a new client context.
|
|
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec, cssMode bool) *Client {
|
|
return &Client{
|
|
c: esbuild.NewBuildClient(fs, rs, cssMode),
|
|
}
|
|
}
|
|
|
|
// Process processes a resource with the user provided options.
|
|
func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
|
|
return res.Transform(
|
|
&buildTransformation{c: c, optsm: opts},
|
|
)
|
|
}
|
|
|
|
func (c *Client) transform(opts esbuild.Options, transformCtx *resources.ResourceTransformationCtx) (api.BuildResult, error) {
|
|
if transformCtx.DependencyManager != nil {
|
|
opts.DependencyManager = transformCtx.DependencyManager
|
|
}
|
|
|
|
opts.StdinSourcePath = transformCtx.SourcePath
|
|
|
|
pathSpec := c.c.Spec().PathSpec
|
|
outDir := path.Dir(transformCtx.OutPath)
|
|
if opts.IsCSS && opts.PublicPath == "" {
|
|
// Make file-loader artifact URLs (e.g. fonts, images) absolute relative
|
|
// to the web context root, so they resolve correctly whether the CSS is
|
|
// published as a file or inlined into HTML. See issue #14849.
|
|
// In multihost we build once for all hosts; using the deepest of the
|
|
// per-host base paths keeps the URL reachable on every host (each
|
|
// host's base path is a prefix).
|
|
var basePath string
|
|
if pathSpec.MultihostLongestBasePath != "" {
|
|
basePath = pathSpec.MultihostLongestBasePath
|
|
} else {
|
|
basePath = pathSpec.GetBasePath(false)
|
|
}
|
|
dir := outDir
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
opts.PublicPath = "/" + strings.TrimPrefix(path.Join(basePath, dir), "/")
|
|
}
|
|
|
|
result, err := c.c.Build(opts)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
hasLinkedSourceMap := opts.ExternalOptions.SourceMap == "linked"
|
|
hasSourceMap := hasLinkedSourceMap || opts.ExternalOptions.SourceMap == "external"
|
|
|
|
// Classify output files by path rather than relying on array ordering,
|
|
// which esbuild does not guarantee.
|
|
var mainOutput []byte
|
|
for _, file := range result.OutputFiles {
|
|
basePath := path.Base(filepath.ToSlash(file.Path))
|
|
if strings.HasSuffix(basePath, ".map") {
|
|
if hasSourceMap {
|
|
if err = transformCtx.PublishSourceMap(file.Contents); err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
} else if isStdinEntryOutput(basePath) {
|
|
mainOutput = file.Contents
|
|
} else {
|
|
// File-loader artifact; publish directly.
|
|
if err = publishFileLoaderArtifact(pathSpec, opts.PublicPath, outDir, basePath, file.Contents, transformCtx); err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if mainOutput == nil {
|
|
return result, fmt.Errorf("esbuild: entry point output not found")
|
|
}
|
|
|
|
if hasLinkedSourceMap {
|
|
symPath := path.Base(transformCtx.OutPath) + ".map"
|
|
if opts.IsCSS {
|
|
re := regexp.MustCompile(`/\*# sourceMappingURL=.*\n?`)
|
|
mainOutput = re.ReplaceAll(mainOutput, []byte("/*# sourceMappingURL="+symPath+" */\n"))
|
|
} else {
|
|
re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
|
|
mainOutput = re.ReplaceAll(mainOutput, []byte("//# sourceMappingURL="+symPath+"\n"))
|
|
}
|
|
}
|
|
|
|
if _, err = transformCtx.To.Write(mainOutput); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// isStdinEntryOutput reports whether basePath is the entry-point output
|
|
// generated by esbuild when using stdin mode (e.g. "stdin.js", "stdin.css").
|
|
func isStdinEntryOutput(basePath string) bool {
|
|
return strings.HasPrefix(basePath, "stdin.")
|
|
}
|
|
|
|
// publishFileLoaderArtifact writes a file-loader artifact (e.g. a font or image
|
|
// referenced from CSS) to the publish directory. In multihost mode the URL is
|
|
// the same on all hosts, but each host has its own base path; the file is
|
|
// placed where each host's URL resolution will find it.
|
|
func publishFileLoaderArtifact(pathSpec *helpers.PathSpec, publicPath, outDir, basePath string, content []byte, transformCtx *resources.ResourceTransformationCtx) error {
|
|
if len(pathSpec.MultihostTargetBasePaths) == 0 {
|
|
return transformCtx.PublishTo(path.Join(outDir, basePath), content)
|
|
}
|
|
|
|
urlPath := path.Join(publicPath, basePath)
|
|
if !strings.HasPrefix(urlPath, "/") {
|
|
urlPath = "/" + urlPath
|
|
}
|
|
filenames := make([]string, len(pathSpec.MultihostTargetBasePaths))
|
|
for i, langPrefix := range pathSpec.MultihostTargetBasePaths {
|
|
filenames[i] = filepath.FromSlash(langPrefix + strings.TrimPrefix(urlPath, pathSpec.MultihostBasePaths[i]))
|
|
}
|
|
fw, err := helpers.OpenFilesForWriting(pathSpec.BaseFs.PublishFs, filenames...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fw.Close()
|
|
_, err = fw.Write(content)
|
|
return err
|
|
}
|