diff --git a/common/maps/ordered.go b/common/maps/ordered.go index eaa4d73c611..864ba9b32bb 100644 --- a/common/maps/ordered.go +++ b/common/maps/ordered.go @@ -33,6 +33,15 @@ func NewOrdered[K comparable, T any]() *Ordered[K, T] { return &Ordered[K, T]{values: make(map[K]T)} } +// Contains returns whether the map contains the given key. +func (m *Ordered[K, T]) Contains(key K) bool { + if m == nil { + return false + } + _, found := m.values[key] + return found +} + // Set sets the value for the given key. // Note that insertion order is not affected if a key is re-inserted into the map. func (m *Ordered[K, T]) Set(key K, value T) { diff --git a/hugofs/dirsmerger.go b/hugofs/dirsmerger.go index 9eedb584438..eafde205f98 100644 --- a/hugofs/dirsmerger.go +++ b/hugofs/dirsmerger.go @@ -63,3 +63,41 @@ var AppendDirsMerger overlayfs.DirsMerger = func(lofi, bofi []fs.DirEntry) []fs. return lofi } + +// DirsMergerPreserveDuplicateFunc returns a DirsMerger that will preserve any duplicate +// as defined by the given func. +func DirsMergerPreserveDuplicateFunc(preserveDuplicate func(fs.DirEntry) bool) overlayfs.DirsMerger { + return func(lofi, bofi []fs.DirEntry) []fs.DirEntry { + for _, fi1 := range bofi { + var found bool + if !preserveDuplicate(fi1) { + for _, fi2 := range lofi { + if fi1.Name() == fi2.Name() { + found = true + break + } + } + } + if !found { + lofi = append(lofi, fi1) + } + } + return lofi + } +} + +var FuncDirsMerger2 overlayfs.DirsMerger = func(lofi, bofi []fs.DirEntry) []fs.DirEntry { + for _, bofi := range bofi { + var found bool + for _, lofi := range lofi { + if bofi.Name() == lofi.Name() { + found = true + break + } + } + if !found { + lofi = append(lofi, bofi) + } + } + return lofi +} diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go index 0099497624c..1e317c325f4 100644 --- a/hugofs/files/classifier.go +++ b/hugofs/files/classifier.go @@ -47,8 +47,7 @@ const ( ComponentFolderAssets = "assets" ComponentFolderI18n = "i18n" - FolderVendor = "_vendor" - + FolderVendor = "_vendor" FolderResources = "resources" // TODO1 remove. FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc. diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index 38849317492..44a0c4e4803 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -60,6 +60,10 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { for _, rm := range rms { (&rm).clean() + if rm.From == files.FolderVendor { + continue + } + rm.FromBase = files.ResolveComponentFolder(rm.From) if len(rm.To) < 2 { diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index 89626b20012..986a2698cc0 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -18,6 +18,7 @@ package filesystems import ( "fmt" "io" + "io/fs" "os" "path/filepath" "strings" @@ -235,9 +236,6 @@ type SourceFilesystems struct { Archetypes *SourceFilesystem Assets *SourceFilesystem - // Note that this can not be mounted. It's currently fixed at the project root. - Vendor *SourceFilesystem - AssetsWithDuplicatesPreserved *SourceFilesystem RootFss []*hugofs.RootMappingFs @@ -246,6 +244,10 @@ type SourceFilesystems struct { // with any sub module's resource fs layered below. ResourcesCache afero.Fs // TODO1 remove this. + // A writable filesystem on top of the project's vendor directory + // with any sub module's vendor fs layered. + VendorFs afero.Fs + // The work folder (may be a composite of project and theme components). Work afero.Fs @@ -572,13 +574,13 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { b.result.Layouts = createView(files.ComponentFolderLayouts, b.theBigFs.overlayMounts) b.result.Assets = createView(files.ComponentFolderAssets, b.theBigFs.overlayMounts) b.result.ResourcesCache = b.theBigFs.overlayResources + b.result.VendorFs = b.theBigFs.overlayVendor b.result.RootFss = b.theBigFs.rootFss // data and i18n needs a different merge strategy. overlayMountsPreserveDupes := b.theBigFs.overlayMounts.WithDirsMerger(hugofs.AppendDirsMerger) b.result.Data = createView(files.ComponentFolderData, overlayMountsPreserveDupes) b.result.I18n = createView(files.ComponentFolderI18n, overlayMountsPreserveDupes) - b.result.Vendor = createView(files.FolderVendor, overlayMountsPreserveDupes) b.result.AssetsWithDuplicatesPreserved = createView(files.ComponentFolderAssets, overlayMountsPreserveDupes) contentFs := hugofs.NewComponentFs( @@ -632,6 +634,9 @@ func (b *sourceFilesystemsBuilder) createMainOverlayFs(p *paths.Paths) (*filesys overlayMountsStatic: overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}), overlayFull: overlayfs.New(overlayfs.Options{}), overlayResources: overlayfs.New(overlayfs.Options{FirstWritable: true}), + overlayVendor: overlayfs.New(overlayfs.Options{FirstWritable: true, DirsMerger: hugofs.DirsMergerPreserveDuplicateFunc(func(fi fs.DirEntry) bool { + return fi.Name() == "resources.json" + })}), } mods := p.AllModules() @@ -682,6 +687,7 @@ func (b *sourceFilesystemsBuilder) createOverlayFs( collector.overlayMountsFull = appendNopIfEmpty(collector.overlayMountsFull) collector.overlayFull = appendNopIfEmpty(collector.overlayFull) collector.overlayResources = appendNopIfEmpty(collector.overlayResources) + collector.overlayVendor = appendNopIfEmpty(collector.overlayVendor) return nil } @@ -700,7 +706,16 @@ func (b *sourceFilesystemsBuilder) createOverlayFs( return md.dir, hpaths.AbsPathify(md.dir, path) } + modBase := collector.sourceProject + if !md.isMainProject { + modBase = collector.sourceModules + } + for i, mount := range md.Mounts() { + if mount.Target == files.FolderVendor { + collector.overlayVendor = collector.overlayVendor.Append(hugofs.NewBasePathFs(modBase, mount.Source)) + continue + } // Add more weight to early mounts. // When two mounts contain the same filename, // the first entry wins. @@ -748,11 +763,6 @@ func (b *sourceFilesystemsBuilder) createOverlayFs( } } - modBase := collector.sourceProject - if !md.isMainProject { - modBase = collector.sourceModules - } - sourceStatic := modBase rmfs, err := hugofs.NewRootMappingFs(modBase, fromTo...) @@ -835,7 +845,8 @@ type filesystemsCollector struct { overlayMountsStatic *overlayfs.OverlayFs overlayMountsFull *overlayfs.OverlayFs overlayFull *overlayfs.OverlayFs - overlayResources *overlayfs.OverlayFs + overlayResources *overlayfs.OverlayFs // TODO1 remove + overlayVendor *overlayfs.OverlayFs rootFss []*hugofs.RootMappingFs diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index daa91c641e0..795eb346c9b 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -709,10 +709,6 @@ func (h *HugoSites) writeVendor(l logg.LevelLogger) error { return err } - for _, vf := range v.VendoredResources { - fmt.Println("=== ==", vf.Path, vf.Hash) - } - return nil } diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index bee645e5b28..36a4f1d253d 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -572,6 +572,11 @@ func (s *IntegrationTestBuilder) AddFiles(filenameContent ...string) *Integratio return s } +func (s *IntegrationTestBuilder) RemovePublic() *IntegrationTestBuilder { + s.Assert(s.fs.WorkingDirWritable.RemoveAll("public"), qt.IsNil) + return s +} + func (s *IntegrationTestBuilder) RemoveFiles(filenames ...string) *IntegrationTestBuilder { for _, filename := range filenames { absFilename := s.absFilename(filename) diff --git a/modules/client.go b/modules/client.go index d16af2d351b..732e686f301 100644 --- a/modules/client.go +++ b/modules/client.go @@ -270,6 +270,7 @@ func (c *Client) Vendor() error { } // Include the resource cache if present. + // TODO1 remove resourcesDir := filepath.Join(dir, files.FolderResources) _, err := c.fs.Stat(resourcesDir) if err == nil { diff --git a/modules/collect.go b/modules/collect.go index 7034a6b161f..288831e6701 100644 --- a/modules/collect.go +++ b/modules/collect.go @@ -400,6 +400,11 @@ func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error { return err } + mounts, err = c.mountVendorDir(mod, mounts) + if err != nil { + return err + } + mod.mounts = mounts return nil } @@ -597,6 +602,26 @@ func (c *collector) loadModules() error { // Matches postcss.config.js etc. var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`) +func (c *collector) mountVendorDir(owner *moduleAdapter, mounts []Mount) ([]Mount, error) { + dir := filepath.Join(owner.Dir(), files.FolderVendor) + + add := owner.projectMod + if !add { + if _, err := c.fs.Stat(files.FolderVendor); err == nil { + add = true + } + } + + if add { + mounts = append(mounts, Mount{ + Source: dir, + Target: files.FolderVendor, + }) + } + + return mounts, nil +} + func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) { for _, m := range mounts { if strings.HasPrefix(m.Target, files.JsConfigFolderMountPrefix) { diff --git a/resources/internal/vendor/vendor.go b/resources/internal/vendor/vendor.go index 649aab595d6..dc2895f1163 100644 --- a/resources/internal/vendor/vendor.go +++ b/resources/internal/vendor/vendor.go @@ -14,20 +14,27 @@ package vendor import ( - "encoding/hex" + "encoding/json" + "fmt" "io" + "io/fs" + "path" "path/filepath" "sort" "strings" + "sync" "github.com/cespare/xxhash/v2" + "github.com/gobwas/glob" + "github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/mitchellh/mapstructure" "github.com/spf13/afero" - "github.com/spf13/cast" ) var _ io.Closer = (*ResourceVendor)(nil) @@ -41,19 +48,29 @@ type Vendorable interface { // Make this shallow so a transformation may be shared between environments, if needed, // do not include any filenames/content hashes here. // Typically you can use the VendorKeyFromOpts function to extract this from the options. - VendorKey() string + VendorScope() map[string]any } -// VendorKeyFromOpts extracts the vendor key from the given options map. -func VendorKeyFromOpts(m map[string]any) string { - const vendorKeyKey = "vendorKey" +type VendorScope struct { + // An optional key that identifies the resource transformation variant. + // The default vendor path is deliberately shallow, so this allows multiple vendored variants of the same resource transformation + // with different configurations. + Key string `json:"key"` + + // A Glob pattern matching the build environment, e.g. “{production,development}” or “*”. The default is “*”. + Environment string `json:"environment"` +} + +// VendorScopeFromOpts extracts the vendor scoped from the given options map. +func VendorScopeFromOpts(m map[string]any) map[string]any { + const vendorScopeKey = "vendorScope" for k, v := range m { - if strings.EqualFold(k, vendorKeyKey) { - return cast.ToString(v) + if strings.EqualFold(k, vendorScopeKey) { + return maps.ToStringMap(v) } } - return "" + return nil } type ResourceVendor struct { @@ -76,25 +93,122 @@ func (v *ResourceVendor) Close() error { return nil } -func NewVendorer(fs afero.Fs, workingDir string) *Vendorer { - return &Vendorer{ - fs: fs, - workingDir: workingDir, +func NewVendorer(vendorFs, sourceFs afero.Fs, environment string) (*ResourceVendorer, error) { + rv := &ResourceVendorer{ + vendorFs: vendorFs, + sourceFs: sourceFs, + environment: environment, + } + + if err := rv.init(); err != nil { + return nil, err + } + + return rv, nil +} + +type ResourceVendorer struct { + vendorFs afero.Fs // Fs relative to the vendor root, top layer writatble. + sourceFs afero.Fs // Usually OS filesystem. + environment string + + mu sync.Mutex + + output outputResources + vendoredResources map[string]*maps.Ordered[string, vendoredResource] +} + +type outputResources struct { + Resources []outputResource `json:"resources"` +} + +func (v *ResourceVendorer) init() error { + dir, err := v.vendorFs.Open(vendorResources) + if err != nil { + if herrors.IsNotExist(err) { + return nil + } + return err + } + defer dir.Close() + fis, err := dir.(fs.ReadDirFile).ReadDir(-1) + if err != nil { + return err + } + + v.mu.Lock() + defer v.mu.Unlock() + v.vendoredResources = make(map[string]*maps.Ordered[string, vendoredResource]) + + for _, de := range fis { + if de.Name() == vendorResourcesJSON { + fim := de.(hugofs.FileMetaInfo) + f, err := fim.Meta().Open() + if err != nil { + return err + } + defer f.Close() + + var resources outputResources + if err := json.NewDecoder(f).Decode(&resources); err != nil { + return err + } + vendorDir := filepath.Dir(fim.Meta().Filename) + for _, r := range resources.Resources { + vr := vendoredResource{ + resource: r, + vendorDir: vendorDir, + } + m := v.vendoredResources[r.BasePath] + if m == nil { + m = maps.NewOrdered[string, vendoredResource]() + v.vendoredResources[r.BasePath] = m + } + if !m.Contains(r.ScopeHash) { + if err := vr.init(); err != nil { + return err + } + m.Set(r.ScopeHash, vr) + } + } + + } } + return nil } -type Vendorer struct { - fs afero.Fs - workingDir string +type vendoredResource struct { + resource outputResource + vendorDir string + + matchEnvironment glob.Glob +} - VendoredResources []VendoredResource +func (v *vendoredResource) init() error { + v.matchEnvironment = glob.MustCompile(v.resource.Scope.Environment) + return nil } -func (v *Vendorer) Finalize() error { +func (v *ResourceVendorer) Finalize() error { // Sort the vendored resources to make the order deterministic. - sort.Slice(v.VendoredResources, func(i, j int) bool { - return v.VendoredResources[i].Path < v.VendoredResources[j].Path + sort.Slice(v.output.Resources, func(i, j int) bool { + return v.output.Resources[i].BasePath < v.output.Resources[j].BasePath }) + + vendorFilename := filepath.Join(vendorResources, vendorResourcesJSON) + + f, err := v.vendorFs.Create(vendorFilename) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + if err := enc.Encode(v.output); err != nil { + return err + } + return nil } @@ -107,67 +221,70 @@ type ResourceVendorOptions struct { } const ( - VendorRoot = "_vendor" - vendorModules = "mod" - vendorResources = "res" - vendorScopeAll = "_all" + vendorRoot = "_vendor" + vendorModules = "modules" + vendorResources = "resources" // TODO1 + modules. + vendorResourcesJSON = "resources.json" ) -type VendoredResource struct { +type outputResource struct { + // BasePath to the vendored resource, relative to the vendor root. + // Unix style path. + // The full path starting from the vendor root is BasePath/ScopeHash/Path. + BasePath string `json:"basePath"` + + // Path the last path element of the vendored resource. Path string `json:"path"` - Hash string `json:"hash"` + + Scope VendorScope `json:"scope"` + ScopeHash string `json:"scopeHash"` } -// TODO1 add --vendorScope=* or {development,production} e.g. Encode it into the root folder. Or: At least create a "all" root folder for future use. -// add resources.txt to root with file/hash listing + version. -// TODO1 remove me. -func (v *Vendorer) NewResourceVendor(opts ResourceVendorOptions) (*ResourceVendor, error) { - digest := xxhash.New() - finalFrom := io.TeeReader(opts.From, digest) - vendorFileName := filepath.Join(v.workingDir, VendorRoot, vendorResources, vendorScopeAll, opts.Target.VendorName(), opts.Target.VendorKey(), opts.InPath) - vendoredFile, err := helpers.OpenFileForWriting(v.fs, vendorFileName) - if err != nil { - return nil, err +func (v *ResourceVendorer) OpenVendoredFileForWriting(opts ResourceVendorOptions) (io.WriteCloser, hugio.OpenReadSeekCloser, error) { + vs := opts.Target.VendorScope() + scopeHash := hashing.HashStringHex(vs) + vendorScope := VendorScope{ + Environment: "*", + } + if err := mapstructure.WeakDecode(vs, &vendorScope); err != nil { + return nil, nil, err } - finalTo := io.MultiWriter(opts.To, vendoredFile) - return &ResourceVendor{ - Digest: digest, - FinalFrom: finalFrom, - FinalTo: finalTo, - closeFunc: vendoredFile.Close, - }, nil -} -func (v *Vendorer) OpenVendoredFileForWriting(opts ResourceVendorOptions) (io.WriteCloser, hugio.OpenReadSeekCloser, error) { - // TODO1 make the fs relative to the working dir. - vendorFileName := v.vendorFilename(opts) - vendorFilePath := paths.TrimLeading(filepath.ToSlash(strings.TrimPrefix(vendorFileName, v.workingDir))) - f, err := helpers.OpenFileForWriting(v.fs, vendorFileName) + vendorBasePath := v.vendorPath(opts) + vendorDir := filepath.Join(vendorBasePath, scopeHash) + if err := v.vendorFs.MkdirAll(vendorDir, 0o755); err != nil { + return nil, nil, fmt.Errorf("failed to create directory %q: %w", vendorBasePath, err) + } + vendorFilename := filepath.Join(vendorDir, opts.InPath) + f, err := helpers.OpenFileForWriting(v.vendorFs, vendorFilename) if err != nil { return nil, nil, err } - open := v.VendoredOpenReadSeekCloser(opts) - h := xxhash.New() + open := func() (hugio.ReadSeekCloser, error) { + return v.vendorFs.Open(vendorFilename) + } + closer := types.CloserFunc(func() error { - hash := h.Sum(nil) - vendoredResource := VendoredResource{ - Path: vendorFilePath, - Hash: hex.EncodeToString(hash), + vendoredResource := outputResource{ + BasePath: vendorBasePath, + Path: opts.InPath, + Scope: vendorScope, + ScopeHash: scopeHash, } - v.VendoredResources = append(v.VendoredResources, vendoredResource) - return nil + v.mu.Lock() + v.output.Resources = append(v.output.Resources, vendoredResource) + v.mu.Unlock() + + return f.Close() }) - w := hugio.NewMultiWriteCloser( - f, - hugio.NewWriteCloser(h, closer), - ) + w := hugio.NewWriteCloser(f, closer) return w, open, nil } // OpenVendoredFile opens a vendored file for reading or nil if not found. -func (v *Vendorer) OpenVendoredFile(opts ResourceVendorOptions) (hugio.ReadSeekCloser, hugio.OpenReadSeekCloser, error) { +func (v *ResourceVendorer) OpenVendoredFile(opts ResourceVendorOptions) (hugio.ReadSeekCloser, hugio.OpenReadSeekCloser, error) { open := v.VendoredOpenReadSeekCloser(opts) f, err := open() if err != nil { @@ -179,15 +296,34 @@ func (v *Vendorer) OpenVendoredFile(opts ResourceVendorOptions) (hugio.ReadSeekC return f, open, nil } -func (v *Vendorer) VendoredOpenReadSeekCloser(opts ResourceVendorOptions) hugio.OpenReadSeekCloser { +func (v *ResourceVendorer) VendoredOpenReadSeekCloser(opts ResourceVendorOptions) hugio.OpenReadSeekCloser { + vendorFilePath := v.vendorPath(opts) + + r, found := v.vendoredResources[vendorFilePath] + return func() (hugio.ReadSeekCloser, error) { - vendorFileName := v.vendorFilename(opts) - return v.fs.Open(vendorFileName) + if !found { + return nil, afero.ErrFileNotFound + } + + var filename string + r.Range(func(key string, vr vendoredResource) bool { + if vr.matchEnvironment.Match(v.environment) { + r := vr.resource + filename = filepath.Join(vr.vendorDir, vendorFilePath, r.ScopeHash, r.Path) + return false + } + return true + }) + if filename == "" { + return nil, afero.ErrFileNotFound + } + // resources/css/tailwindcss + return v.sourceFs.Open(filename) } } -func (v *Vendorer) vendorFilename(opts ResourceVendorOptions) string { - vendorKey := paths.NormalizePathStringBasic(opts.Target.VendorKey()) - n := filepath.Join(v.workingDir, VendorRoot, vendorResources, vendorScopeAll, opts.Target.VendorName(), vendorKey, opts.InPath) +func (v *ResourceVendorer) vendorPath(opts ResourceVendorOptions) string { + n := filepath.ToSlash(path.Join(vendorResources, opts.Target.VendorName())) return n } diff --git a/resources/resource_spec.go b/resources/resource_spec.go index b84723d307d..1b2250cc12c 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -92,6 +92,11 @@ func NewSpec( } } + vend, err := vendor.NewVendorer(s.BaseFs.VendorFs, s.BaseFs.SourceFs, conf.Environment) + if err != nil { + return nil, err + } + rs := &Spec{ PathSpec: s, Logger: logger, @@ -104,7 +109,7 @@ func NewSpec( memCache, s, ), - Vendorer: vendor.NewVendorer(s.SourceFs, s.Cfg.WorkingDir()), + Vendorer: vend, ExecHelper: execHelper, Permalinks: permalinks, @@ -124,7 +129,7 @@ type Spec struct { ErrorSender herrors.ErrorSender BuildClosers types.CloseAdder Rebuilder identity.SignalRebuilder - Vendorer *vendor.Vendorer + Vendorer *vendor.ResourceVendorer TextTemplates tpl.TemplateParseFinder diff --git a/resources/resource_transformers/cssjs/tailwindcss.go b/resources/resource_transformers/cssjs/tailwindcss.go index cee5ca08247..c8cc0d7a495 100644 --- a/resources/resource_transformers/cssjs/tailwindcss.go +++ b/resources/resource_transformers/cssjs/tailwindcss.go @@ -63,8 +63,8 @@ func (t *tailwindcssTransformation) VendorName() string { return "css/tailwindcss" } -func (t *tailwindcssTransformation) VendorKey() string { - return vendor.VendorKeyFromOpts(t.optionsm) +func (t *tailwindcssTransformation) VendorScope() map[string]any { + return vendor.VendorScopeFromOpts(t.optionsm) } func (t *tailwindcssTransformation) Key() internal.ResourceTransformationKey { diff --git a/resources/resource_transformers/cssjs/tailwindcss_integration_test.go b/resources/resource_transformers/cssjs/tailwindcss_integration_test.go index fac483036f6..9c9617650ce 100644 --- a/resources/resource_transformers/cssjs/tailwindcss_integration_test.go +++ b/resources/resource_transformers/cssjs/tailwindcss_integration_test.go @@ -14,11 +14,12 @@ package cssjs_test import ( + "fmt" + "strings" "testing" - qt "github.com/frankban/quicktest" - "github.com/bep/logg" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" ) @@ -73,16 +74,16 @@ CSS: {{ $css.RelPermalink }}|{{ $css.Content | safeCSS }}| b.AssertFileContent("public/index.html", "/*! tailwindcss v4.0.0") } -func TestTailwindV4BasicVendor(t *testing.T) { +func TestTailwindV4BasicVendorRoundTrip(t *testing.T) { if !htesting.IsCI() { t.Skip("Skip long running test when running locally") } - files := ` + filesTemplate := ` -- hugo.toml -- baseURL = "https://example.org/" [internal] -vendor = true +vendor = VENDOR -- package.json -- { "license": "MIT", @@ -110,23 +111,35 @@ vendor = true --color-neon-cyan: oklch(91.3% 0.139 195.8); } -- layouts/index.html -- -{{ $css := resources.Get "css/styles.css" | css.TailwindCSS }} +{{ $css := resources.Get "css/styles.css" | css.TailwindCSS (dict "vendorScope" (dict "key" "foobar" ) ) }} CSS: {{ $css.RelPermalink }}|{{ $css.Content | safeCSS }}| ` - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - NeedsOsFS: true, - NeedsNpmInstall: true, - PrintAndKeepTempDir: false, - LogLevel: logg.LevelError, - }).Build() + workinDir := t.TempDir() - b.Assert(b.H.Conf.Vendor(), qt.IsTrue) + for _, vendor := range []bool{true, false} { + files := strings.Replace(filesTemplate, "VENDOR", fmt.Sprint(vendor), 1) - b.AssertWorkingDir("_vendor", "_vendor/res/_all/css/tailwindcss/css/styles.css") + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + NeedsOsFS: true, + NeedsNpmInstall: true, + WorkingDir: workinDir, + LogLevel: logg.LevelError, + }).Build() - b.AssertFileContent("public/index.html", "/*! tailwindcss v4.0.0") + b.Assert(b.H.Conf.Vendor(), qt.Equals, vendor) + + b.AssertWorkingDir("_vendor", "_vendor/resources/css/tailwindcss/22ae68a32f191359/css/styles.css") + + // b.AssertWorkingDir("_vendor", "_vendor/resources/_all/css/tailwindcss/css/styles.css") + + b.AssertFileContent("public/index.html", "/*! tailwindcss v4.0.0") + b.AssertFileContent("_vendor/resources/resources.json", "22ae68a32f191359") + + b.RemovePublic() + + } } diff --git a/resources/resource_transformers/js/transform.go b/resources/resource_transformers/js/transform.go index 6364ce942da..44a04ebbd7a 100644 --- a/resources/resource_transformers/js/transform.go +++ b/resources/resource_transformers/js/transform.go @@ -36,8 +36,8 @@ func (t *buildTransformation) VendorName() string { return "js/build" } -func (t *buildTransformation) VendorKey() string { - return vendor.VendorKeyFromOpts(t.optsm) +func (t *buildTransformation) VendorScope() map[string]any { + return vendor.VendorScopeFromOpts(t.optsm) } func (t *buildTransformation) Key() internal.ResourceTransformationKey {