hugo_modules_test.go (29641B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "fmt" 18 "math/rand" 19 "os" 20 "path/filepath" 21 "strings" 22 "testing" 23 "time" 24 25 "github.com/gohugoio/hugo/config" 26 "github.com/gohugoio/hugo/modules/npm" 27 28 "github.com/gohugoio/hugo/common/loggers" 29 30 "github.com/spf13/afero" 31 32 "github.com/gohugoio/hugo/hugofs/files" 33 34 "github.com/gohugoio/hugo/common/hugo" 35 36 "github.com/gohugoio/hugo/htesting" 37 "github.com/gohugoio/hugo/hugofs" 38 39 qt "github.com/frankban/quicktest" 40 "github.com/gohugoio/testmodBuilder/mods" 41 ) 42 43 func TestHugoModulesVariants(t *testing.T) { 44 if !htesting.IsCI() { 45 t.Skip("skip (relative) long running modules test when running locally") 46 } 47 48 tomlConfig := ` 49 baseURL="https://example.org" 50 workingDir = %q 51 52 [module] 53 [[module.imports]] 54 path="github.com/gohugoio/hugoTestModule2" 55 %s 56 ` 57 58 createConfig := func(workingDir, moduleOpts string) string { 59 return fmt.Sprintf(tomlConfig, workingDir, moduleOpts) 60 } 61 62 newTestBuilder := func(t testing.TB, moduleOpts string) *sitesBuilder { 63 b := newTestSitesBuilder(t) 64 tempDir := t.TempDir() 65 workingDir := filepath.Join(tempDir, "myhugosite") 66 b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil) 67 cfg := config.NewWithTestDefaults() 68 cfg.Set("workingDir", workingDir) 69 b.Fs = hugofs.NewDefault(cfg) 70 b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts)) 71 b.WithTemplates( 72 "index.html", ` 73 Param from module: {{ site.Params.Hugo }}| 74 {{ $js := resources.Get "jslibs/alpinejs/alpine.js" }} 75 JS imported in module: {{ with $js }}{{ .RelPermalink }}{{ end }}| 76 `, 77 "_default/single.html", `{{ .Content }}`) 78 b.WithContent("p1.md", `--- 79 title: "Page" 80 --- 81 82 [A link](https://bep.is) 83 84 `) 85 b.WithSourceFile("go.mod", ` 86 module github.com/gohugoio/tests/testHugoModules 87 88 89 `) 90 91 b.WithSourceFile("go.sum", ` 92 github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877 h1:WLM2bQCKIWo04T6NsIWsX/Vtirhf0TnpY66xyqGlgVY= 93 github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877/go.mod h1:CBFZS3khIAXKxReMwq0le8sEl/D8hcXmixlOHVv+Gd0= 94 `) 95 96 return b 97 } 98 99 t.Run("Target in subfolder", func(t *testing.T) { 100 b := newTestBuilder(t, "ignoreImports=true") 101 b.Build(BuildCfg{}) 102 103 b.AssertFileContent("public/p1/index.html", `<p>Page|https://bep.is|Title: |Text: A link|END</p>`) 104 }) 105 106 t.Run("Ignore config", func(t *testing.T) { 107 b := newTestBuilder(t, "ignoreConfig=true") 108 b.Build(BuildCfg{}) 109 110 b.AssertFileContent("public/index.html", ` 111 Param from module: | 112 JS imported in module: | 113 `) 114 }) 115 116 t.Run("Ignore imports", func(t *testing.T) { 117 b := newTestBuilder(t, "ignoreImports=true") 118 b.Build(BuildCfg{}) 119 120 b.AssertFileContent("public/index.html", ` 121 Param from module: Rocks| 122 JS imported in module: | 123 `) 124 }) 125 126 t.Run("Create package.json", func(t *testing.T) { 127 b := newTestBuilder(t, "") 128 129 b.WithSourceFile("package.json", `{ 130 "name": "mypack", 131 "version": "1.2.3", 132 "scripts": { 133 "client": "wait-on http://localhost:1313 && open http://localhost:1313", 134 "start": "run-p client server", 135 "test": "echo 'hoge' > hoge" 136 }, 137 "dependencies": { 138 "nonon": "error" 139 } 140 }`) 141 142 b.WithSourceFile("package.hugo.json", `{ 143 "name": "mypack", 144 "version": "1.2.3", 145 "scripts": { 146 "client": "wait-on http://localhost:1313 && open http://localhost:1313", 147 "start": "run-p client server", 148 "test": "echo 'hoge' > hoge" 149 }, 150 "dependencies": { 151 "foo": "1.2.3" 152 }, 153 "devDependencies": { 154 "postcss-cli": "7.8.0", 155 "tailwindcss": "1.8.0" 156 157 } 158 }`) 159 160 b.Build(BuildCfg{}) 161 b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil) 162 163 b.AssertFileContentFn("package.json", func(s string) bool { 164 return s == `{ 165 "comments": { 166 "dependencies": { 167 "foo": "project", 168 "react-dom": "github.com/gohugoio/hugoTestModule2" 169 }, 170 "devDependencies": { 171 "@babel/cli": "github.com/gohugoio/hugoTestModule2", 172 "@babel/core": "github.com/gohugoio/hugoTestModule2", 173 "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", 174 "postcss-cli": "project", 175 "tailwindcss": "project" 176 } 177 }, 178 "dependencies": { 179 "foo": "1.2.3", 180 "react-dom": "^16.13.1" 181 }, 182 "devDependencies": { 183 "@babel/cli": "7.8.4", 184 "@babel/core": "7.9.0", 185 "@babel/preset-env": "7.9.5", 186 "postcss-cli": "7.8.0", 187 "tailwindcss": "1.8.0" 188 }, 189 "name": "mypack", 190 "scripts": { 191 "client": "wait-on http://localhost:1313 && open http://localhost:1313", 192 "start": "run-p client server", 193 "test": "echo 'hoge' > hoge" 194 }, 195 "version": "1.2.3" 196 } 197 ` 198 }) 199 }) 200 201 t.Run("Create package.json, no default", func(t *testing.T) { 202 b := newTestBuilder(t, "") 203 204 const origPackageJSON = `{ 205 "name": "mypack", 206 "version": "1.2.3", 207 "scripts": { 208 "client": "wait-on http://localhost:1313 && open http://localhost:1313", 209 "start": "run-p client server", 210 "test": "echo 'hoge' > hoge" 211 }, 212 "dependencies": { 213 "moo": "1.2.3" 214 } 215 }` 216 217 b.WithSourceFile("package.json", origPackageJSON) 218 219 b.Build(BuildCfg{}) 220 b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil) 221 222 b.AssertFileContentFn("package.json", func(s string) bool { 223 return s == `{ 224 "comments": { 225 "dependencies": { 226 "moo": "project", 227 "react-dom": "github.com/gohugoio/hugoTestModule2" 228 }, 229 "devDependencies": { 230 "@babel/cli": "github.com/gohugoio/hugoTestModule2", 231 "@babel/core": "github.com/gohugoio/hugoTestModule2", 232 "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", 233 "postcss-cli": "github.com/gohugoio/hugoTestModule2", 234 "tailwindcss": "github.com/gohugoio/hugoTestModule2" 235 } 236 }, 237 "dependencies": { 238 "moo": "1.2.3", 239 "react-dom": "^16.13.1" 240 }, 241 "devDependencies": { 242 "@babel/cli": "7.8.4", 243 "@babel/core": "7.9.0", 244 "@babel/preset-env": "7.9.5", 245 "postcss-cli": "7.1.0", 246 "tailwindcss": "1.2.0" 247 }, 248 "name": "mypack", 249 "scripts": { 250 "client": "wait-on http://localhost:1313 && open http://localhost:1313", 251 "start": "run-p client server", 252 "test": "echo 'hoge' > hoge" 253 }, 254 "version": "1.2.3" 255 } 256 ` 257 }) 258 259 // https://github.com/gohugoio/hugo/issues/7690 260 b.AssertFileContent("package.hugo.json", origPackageJSON) 261 }) 262 263 t.Run("Create package.json, no default, no package.json", func(t *testing.T) { 264 b := newTestBuilder(t, "") 265 266 b.Build(BuildCfg{}) 267 b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil) 268 269 b.AssertFileContentFn("package.json", func(s string) bool { 270 return s == `{ 271 "comments": { 272 "dependencies": { 273 "react-dom": "github.com/gohugoio/hugoTestModule2" 274 }, 275 "devDependencies": { 276 "@babel/cli": "github.com/gohugoio/hugoTestModule2", 277 "@babel/core": "github.com/gohugoio/hugoTestModule2", 278 "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", 279 "postcss-cli": "github.com/gohugoio/hugoTestModule2", 280 "tailwindcss": "github.com/gohugoio/hugoTestModule2" 281 } 282 }, 283 "dependencies": { 284 "react-dom": "^16.13.1" 285 }, 286 "devDependencies": { 287 "@babel/cli": "7.8.4", 288 "@babel/core": "7.9.0", 289 "@babel/preset-env": "7.9.5", 290 "postcss-cli": "7.1.0", 291 "tailwindcss": "1.2.0" 292 }, 293 "name": "myhugosite", 294 "version": "0.1.0" 295 } 296 ` 297 }) 298 }) 299 } 300 301 // TODO(bep) this fails when testmodBuilder is also building ... 302 func TestHugoModulesMatrix(t *testing.T) { 303 if !htesting.IsCI() { 304 t.Skip("skip (relative) long running modules test when running locally") 305 } 306 t.Parallel() 307 308 if !htesting.IsCI() || hugo.GoMinorVersion() < 12 { 309 // https://github.com/golang/go/issues/26794 310 // There were some concurrent issues with Go modules in < Go 12. 311 t.Skip("skip this on local host and for Go <= 1.11 due to a bug in Go's stdlib") 312 } 313 314 if testing.Short() { 315 t.Skip() 316 } 317 318 rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 319 gooss := []string{"linux", "darwin", "windows"} 320 goos := gooss[rnd.Intn(len(gooss))] 321 ignoreVendor := rnd.Intn(2) == 0 322 testmods := mods.CreateModules(goos).Collect() 323 rnd.Shuffle(len(testmods), func(i, j int) { testmods[i], testmods[j] = testmods[j], testmods[i] }) 324 325 for _, m := range testmods[:2] { 326 c := qt.New(t) 327 328 workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test") 329 c.Assert(err, qt.IsNil) 330 defer clean() 331 332 v := config.NewWithTestDefaults() 333 v.Set("workingDir", workingDir) 334 335 configTemplate := ` 336 baseURL = "https://example.com" 337 title = "My Modular Site" 338 workingDir = %q 339 theme = %q 340 ignoreVendorPaths = %q 341 342 ` 343 344 ignoreVendorPaths := "" 345 if ignoreVendor { 346 ignoreVendorPaths = "github.com/**" 347 } 348 config := fmt.Sprintf(configTemplate, workingDir, m.Path(), ignoreVendorPaths) 349 350 b := newTestSitesBuilder(t) 351 352 // Need to use OS fs for this. 353 b.Fs = hugofs.NewDefault(v) 354 355 b.WithWorkingDir(workingDir).WithConfigFile("toml", config) 356 b.WithContent("page.md", ` 357 --- 358 title: "Foo" 359 --- 360 `) 361 b.WithTemplates("home.html", ` 362 363 {{ $mod := .Site.Data.modinfo.module }} 364 Mod Name: {{ $mod.name }} 365 Mod Version: {{ $mod.version }} 366 ---- 367 {{ range $k, $v := .Site.Data.modinfo }} 368 - {{ $k }}: {{ range $kk, $vv := $v }}{{ $kk }}: {{ $vv }}|{{ end -}} 369 {{ end }} 370 371 372 `) 373 b.WithSourceFile("go.mod", ` 374 module github.com/gohugoio/tests/testHugoModules 375 376 377 `) 378 379 b.Build(BuildCfg{}) 380 381 // Verify that go.mod is autopopulated with all the modules in config.toml. 382 b.AssertFileContent("go.mod", m.Path()) 383 384 b.AssertFileContent("public/index.html", 385 "Mod Name: "+m.Name(), 386 "Mod Version: v1.4.0") 387 388 b.AssertFileContent("public/index.html", createChildModMatchers(m, ignoreVendor, m.Vendor)...) 389 390 } 391 } 392 393 func createChildModMatchers(m *mods.Md, ignoreVendor, vendored bool) []string { 394 // Child dependencies are one behind. 395 expectMinorVersion := 3 396 397 if !ignoreVendor && vendored { 398 // Vendored modules are stuck at v1.1.0. 399 expectMinorVersion = 1 400 } 401 402 expectVersion := fmt.Sprintf("v1.%d.0", expectMinorVersion) 403 404 var matchers []string 405 406 for _, mm := range m.Children { 407 matchers = append( 408 matchers, 409 fmt.Sprintf("%s: name: %s|version: %s", mm.Name(), mm.Name(), expectVersion)) 410 matchers = append(matchers, createChildModMatchers(mm, ignoreVendor, vendored || mm.Vendor)...) 411 } 412 return matchers 413 } 414 415 func TestModulesWithContent(t *testing.T) { 416 t.Parallel() 417 418 b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` 419 baseURL="https://example.org" 420 421 workingDir="/site" 422 423 defaultContentLanguage = "en" 424 425 [module] 426 [[module.imports]] 427 path="a" 428 [[module.imports.mounts]] 429 source="myacontent" 430 target="content/blog" 431 lang="en" 432 [[module.imports]] 433 path="b" 434 [[module.imports.mounts]] 435 source="mybcontent" 436 target="content/blog" 437 lang="nn" 438 [[module.imports]] 439 path="c" 440 [[module.imports]] 441 path="d" 442 443 [languages] 444 445 [languages.en] 446 title = "Title in English" 447 languageName = "English" 448 weight = 1 449 [languages.nn] 450 languageName = "Nynorsk" 451 weight = 2 452 title = "Tittel på nynorsk" 453 [languages.nb] 454 languageName = "Bokmål" 455 weight = 3 456 title = "Tittel på bokmål" 457 [languages.fr] 458 languageName = "French" 459 weight = 4 460 title = "French Title" 461 462 463 `) 464 465 b.WithTemplatesAdded("index.html", ` 466 {{ range .Site.RegularPages }} 467 |{{ .Title }}|{{ .RelPermalink }}|{{ .Plain }} 468 {{ end }} 469 {{ $data := .Site.Data }} 470 Data Common: {{ $data.common.value }} 471 Data C: {{ $data.c.value }} 472 Data D: {{ $data.d.value }} 473 All Data: {{ $data }} 474 475 i18n hello1: {{ i18n "hello1" . }} 476 i18n theme: {{ i18n "theme" . }} 477 i18n theme2: {{ i18n "theme2" . }} 478 `) 479 480 content := func(id string) string { 481 return fmt.Sprintf(`--- 482 title: Title %s 483 --- 484 Content %s 485 486 `, id, id) 487 } 488 489 i18nContent := func(id, value string) string { 490 return fmt.Sprintf(` 491 [%s] 492 other = %q 493 `, id, value) 494 } 495 496 // Content files 497 b.WithSourceFile("themes/a/myacontent/page.md", content("theme-a-en")) 498 b.WithSourceFile("themes/b/mybcontent/page.md", content("theme-b-nn")) 499 b.WithSourceFile("themes/c/content/blog/c.md", content("theme-c-nn")) 500 501 // Data files 502 b.WithSourceFile("data/common.toml", `value="Project"`) 503 b.WithSourceFile("themes/c/data/common.toml", `value="Theme C"`) 504 b.WithSourceFile("themes/c/data/c.toml", `value="Hugo Rocks!"`) 505 b.WithSourceFile("themes/d/data/c.toml", `value="Hugo Rodcks!"`) 506 b.WithSourceFile("themes/d/data/d.toml", `value="Hugo Rodks!"`) 507 508 // i18n files 509 b.WithSourceFile("i18n/en.toml", i18nContent("hello1", "Project")) 510 b.WithSourceFile("themes/c/i18n/en.toml", ` 511 [hello1] 512 other="Theme C Hello" 513 [theme] 514 other="Theme C" 515 `) 516 b.WithSourceFile("themes/d/i18n/en.toml", i18nContent("theme", "Theme D")) 517 b.WithSourceFile("themes/d/i18n/en.toml", i18nContent("theme2", "Theme2 D")) 518 519 // Static files 520 b.WithSourceFile("themes/c/static/hello.txt", `Hugo Rocks!"`) 521 522 b.Build(BuildCfg{}) 523 524 b.AssertFileContent("public/index.html", "|Title theme-a-en|/blog/page/|Content theme-a-en") 525 b.AssertFileContent("public/nn/index.html", "|Title theme-b-nn|/nn/blog/page/|Content theme-b-nn") 526 527 // Data 528 b.AssertFileContent("public/index.html", 529 "Data Common: Project", 530 "Data C: Hugo Rocks!", 531 "Data D: Hugo Rodks!", 532 ) 533 534 // i18n 535 b.AssertFileContent("public/index.html", 536 "i18n hello1: Project", 537 "i18n theme: Theme C", 538 "i18n theme2: Theme2 D", 539 ) 540 } 541 542 func TestModulesIgnoreConfig(t *testing.T) { 543 b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` 544 baseURL="https://example.org" 545 546 workingDir="/site" 547 548 [module] 549 [[module.imports]] 550 path="a" 551 ignoreConfig=true 552 553 `) 554 555 b.WithSourceFile("themes/a/config.toml", ` 556 [params] 557 a = "Should Be Ignored!" 558 `) 559 560 b.WithTemplatesAdded("index.html", `Params: {{ .Site.Params }}`) 561 562 b.Build(BuildCfg{}) 563 564 b.AssertFileContentFn("public/index.html", func(s string) bool { 565 return !strings.Contains(s, "Ignored") 566 }) 567 } 568 569 func TestModulesDisabled(t *testing.T) { 570 b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` 571 baseURL="https://example.org" 572 573 workingDir="/site" 574 575 [module] 576 [[module.imports]] 577 path="a" 578 [[module.imports]] 579 path="b" 580 disable=true 581 582 583 `) 584 585 b.WithSourceFile("themes/a/config.toml", ` 586 [params] 587 a = "A param" 588 `) 589 590 b.WithSourceFile("themes/b/config.toml", ` 591 [params] 592 b = "B param" 593 `) 594 595 b.WithTemplatesAdded("index.html", `Params: {{ .Site.Params }}`) 596 597 b.Build(BuildCfg{}) 598 599 b.AssertFileContentFn("public/index.html", func(s string) bool { 600 return strings.Contains(s, "A param") && !strings.Contains(s, "B param") 601 }) 602 } 603 604 func TestModulesIncompatible(t *testing.T) { 605 t.Parallel() 606 607 b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` 608 baseURL="https://example.org" 609 610 workingDir="/site" 611 612 [module] 613 [[module.imports]] 614 path="ok" 615 [[module.imports]] 616 path="incompat1" 617 [[module.imports]] 618 path="incompat2" 619 [[module.imports]] 620 path="incompat3" 621 622 `) 623 624 b.WithSourceFile("themes/ok/data/ok.toml", `title = "OK"`) 625 626 b.WithSourceFile("themes/incompat1/config.toml", ` 627 628 [module] 629 [module.hugoVersion] 630 min = "0.33.2" 631 max = "0.45.0" 632 633 `) 634 635 // Old setup. 636 b.WithSourceFile("themes/incompat2/theme.toml", ` 637 min_version = "5.0.0" 638 639 `) 640 641 // Issue 6162 642 b.WithSourceFile("themes/incompat3/theme.toml", ` 643 min_version = 0.55.0 644 645 `) 646 647 logger := loggers.NewWarningLogger() 648 b.WithLogger(logger) 649 650 b.Build(BuildCfg{}) 651 652 c := qt.New(t) 653 654 c.Assert(logger.LogCounters().WarnCounter.Count(), qt.Equals, uint64(3)) 655 } 656 657 func TestModulesSymlinks(t *testing.T) { 658 skipSymlink(t) 659 660 wd, _ := os.Getwd() 661 defer func() { 662 os.Chdir(wd) 663 }() 664 665 c := qt.New(t) 666 workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym") 667 c.Assert(err, qt.IsNil) 668 669 // We need to use the OS fs for this. 670 cfg := config.NewWithTestDefaults() 671 cfg.Set("workingDir", workingDir) 672 fs := hugofs.NewFrom(hugofs.Os, cfg) 673 674 defer clean() 675 676 const homeTemplate = ` 677 Data: {{ .Site.Data }} 678 ` 679 680 createDirsAndFiles := func(baseDir string) { 681 for _, dir := range files.ComponentFolders { 682 realDir := filepath.Join(baseDir, dir, "real") 683 c.Assert(os.MkdirAll(realDir, 0777), qt.IsNil) 684 c.Assert(afero.WriteFile(fs.Source, filepath.Join(realDir, "data.toml"), []byte("[hello]\nother = \"hello\""), 0777), qt.IsNil) 685 } 686 687 c.Assert(afero.WriteFile(fs.Source, filepath.Join(baseDir, "layouts", "index.html"), []byte(homeTemplate), 0777), qt.IsNil) 688 } 689 690 // Create project dirs and files. 691 createDirsAndFiles(workingDir) 692 // Create one module inside the default themes folder. 693 themeDir := filepath.Join(workingDir, "themes", "mymod") 694 createDirsAndFiles(themeDir) 695 696 createSymlinks := func(baseDir, id string) { 697 for _, dir := range files.ComponentFolders { 698 // Issue #9119: private use language tags cannot exceed 8 characters. 699 if dir != "i18n" { 700 c.Assert(os.Chdir(filepath.Join(baseDir, dir)), qt.IsNil) 701 c.Assert(os.Symlink("real", fmt.Sprintf("realsym%s", id)), qt.IsNil) 702 c.Assert(os.Chdir(filepath.Join(baseDir, dir, "real")), qt.IsNil) 703 c.Assert(os.Symlink("data.toml", fmt.Sprintf(filepath.FromSlash("datasym%s.toml"), id)), qt.IsNil) 704 } 705 } 706 } 707 708 createSymlinks(workingDir, "project") 709 createSymlinks(themeDir, "mod") 710 711 config := ` 712 baseURL = "https://example.com" 713 theme="mymod" 714 defaultContentLanguage="nn" 715 defaultContentLanguageInSubDir=true 716 717 [languages] 718 [languages.nn] 719 weight = 1 720 [languages.en] 721 weight = 2 722 723 724 ` 725 726 b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workingDir) 727 b.WithLogger(loggers.NewErrorLogger()) 728 b.Fs = fs 729 730 b.WithConfigFile("toml", config) 731 c.Assert(os.Chdir(workingDir), qt.IsNil) 732 733 b.Build(BuildCfg{}) 734 735 b.AssertFileContentFn(filepath.Join("public", "en", "index.html"), func(s string) bool { 736 // Symbolic links only followed in project. There should be WARNING logs. 737 return !strings.Contains(s, "symmod") && strings.Contains(s, "symproject") 738 }) 739 740 bfs := b.H.BaseFs 741 742 for i, componentFs := range []afero.Fs{ 743 bfs.Static[""].Fs, 744 bfs.Archetypes.Fs, 745 bfs.Content.Fs, 746 bfs.Data.Fs, 747 bfs.Assets.Fs, 748 bfs.I18n.Fs, 749 } { 750 751 if i != 0 { 752 continue 753 } 754 755 for j, id := range []string{"mod", "project"} { 756 757 statCheck := func(fs afero.Fs, filename string, isDir bool) { 758 shouldFail := j == 0 759 if !shouldFail && i == 0 { 760 // Static dirs only supports symlinks for files 761 shouldFail = isDir 762 } 763 764 _, err := fs.Stat(filepath.FromSlash(filename)) 765 if err != nil { 766 if i > 0 && strings.HasSuffix(filename, "toml") && strings.Contains(err.Error(), "files not supported") { 767 // OK 768 return 769 } 770 } 771 772 if shouldFail { 773 c.Assert(err, qt.Not(qt.IsNil)) 774 c.Assert(err, qt.Equals, hugofs.ErrPermissionSymlink) 775 } else { 776 c.Assert(err, qt.IsNil) 777 } 778 } 779 780 c.Logf("Checking %d:%d %q", i, j, id) 781 782 statCheck(componentFs, fmt.Sprintf("realsym%s", id), true) 783 statCheck(componentFs, fmt.Sprintf("real/datasym%s.toml", id), false) 784 785 } 786 } 787 } 788 789 func TestMountsProject(t *testing.T) { 790 t.Parallel() 791 792 config := ` 793 794 baseURL="https://example.org" 795 796 [module] 797 [[module.mounts]] 798 source="mycontent" 799 target="content" 800 801 ` 802 b := newTestSitesBuilder(t). 803 WithConfigFile("toml", config). 804 WithSourceFile(filepath.Join("mycontent", "mypage.md"), ` 805 --- 806 title: "My Page" 807 --- 808 809 `) 810 811 b.Build(BuildCfg{}) 812 813 // helpers.PrintFs(b.H.Fs.Source, "public", os.Stdout) 814 815 b.AssertFileContent("public/mypage/index.html", "Permalink: https://example.org/mypage/") 816 } 817 818 // https://github.com/gohugoio/hugo/issues/6684 819 func TestMountsContentFile(t *testing.T) { 820 t.Parallel() 821 c := qt.New(t) 822 workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-content-file") 823 c.Assert(err, qt.IsNil) 824 defer clean() 825 826 configTemplate := ` 827 baseURL = "https://example.com" 828 title = "My Modular Site" 829 workingDir = %q 830 831 [module] 832 [[module.mounts]] 833 source = "README.md" 834 target = "content/_index.md" 835 [[module.mounts]] 836 source = "mycontent" 837 target = "content/blog" 838 839 ` 840 841 tomlConfig := fmt.Sprintf(configTemplate, workingDir) 842 843 b := newTestSitesBuilder(t).Running() 844 845 cfg := config.NewWithTestDefaults() 846 cfg.Set("workingDir", workingDir) 847 848 b.Fs = hugofs.NewDefault(cfg) 849 850 b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig) 851 b.WithTemplatesAdded("index.html", ` 852 {{ .Title }} 853 {{ .Content }} 854 855 {{ $readme := .Site.GetPage "/README.md" }} 856 {{ with $readme }}README: {{ .Title }}|Filename: {{ path.Join .File.Filename }}|Path: {{ path.Join .File.Path }}|FilePath: {{ path.Join .File.FileInfo.Meta.PathFile }}|{{ end }} 857 858 859 {{ $mypage := .Site.GetPage "/blog/mypage.md" }} 860 {{ with $mypage }}MYPAGE: {{ .Title }}|Path: {{ path.Join .File.Path }}|FilePath: {{ path.Join .File.FileInfo.Meta.PathFile }}|{{ end }} 861 {{ $mybundle := .Site.GetPage "/blog/mybundle" }} 862 {{ with $mybundle }}MYBUNDLE: {{ .Title }}|Path: {{ path.Join .File.Path }}|FilePath: {{ path.Join .File.FileInfo.Meta.PathFile }}|{{ end }} 863 864 865 `, "_default/_markup/render-link.html", ` 866 {{ $link := .Destination }} 867 {{ $isRemote := strings.HasPrefix $link "http" }} 868 {{- if not $isRemote -}} 869 {{ $url := urls.Parse .Destination }} 870 {{ $fragment := "" }} 871 {{- with $url.Fragment }}{{ $fragment = printf "#%s" . }}{{ end -}} 872 {{- with .Page.GetPage $url.Path }}{{ $link = printf "%s%s" .Permalink $fragment }}{{ end }}{{ end -}} 873 <a href="{{ $link | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if $isRemote }} target="_blank"{{ end }}>{{ .Text | safeHTML }}</a> 874 `) 875 876 os.Mkdir(filepath.Join(workingDir, "mycontent"), 0777) 877 os.Mkdir(filepath.Join(workingDir, "mycontent", "mybundle"), 0777) 878 879 b.WithSourceFile("README.md", `--- 880 title: "Readme Title" 881 --- 882 883 Readme Content. 884 `, 885 filepath.Join("mycontent", "mypage.md"), ` 886 --- 887 title: "My Page" 888 --- 889 890 891 * [Relative Link From Page](mybundle) 892 * [Relative Link From Page, filename](mybundle/index.md) 893 * [Link using original path](/mycontent/mybundle/index.md) 894 895 896 `, filepath.Join("mycontent", "mybundle", "index.md"), ` 897 --- 898 title: "My Bundle" 899 --- 900 901 * [Dot Relative Link From Bundle](../mypage.md) 902 * [Link using original path](/mycontent/mypage.md) 903 * [Link to Home](/) 904 * [Link to Home, README.md](/README.md) 905 * [Link to Home, _index.md](/_index.md) 906 907 `) 908 909 b.Build(BuildCfg{}) 910 911 b.AssertFileContent("public/index.html", ` 912 README: Readme Title 913 /README.md|Path: _index.md|FilePath: README.md 914 Readme Content. 915 MYPAGE: My Page|Path: blog/mypage.md|FilePath: mycontent/mypage.md| 916 MYBUNDLE: My Bundle|Path: blog/mybundle/index.md|FilePath: mycontent/mybundle/index.md| 917 `) 918 b.AssertFileContent("public/blog/mypage/index.html", ` 919 <a href="https://example.com/blog/mybundle/">Relative Link From Page</a> 920 <a href="https://example.com/blog/mybundle/">Relative Link From Page, filename</a> 921 <a href="https://example.com/blog/mybundle/">Link using original path</a> 922 923 `) 924 b.AssertFileContent("public/blog/mybundle/index.html", ` 925 <a href="https://example.com/blog/mypage/">Dot Relative Link From Bundle</a> 926 <a href="https://example.com/blog/mypage/">Link using original path</a> 927 <a href="https://example.com/">Link to Home</a> 928 <a href="https://example.com/">Link to Home, README.md</a> 929 <a href="https://example.com/">Link to Home, _index.md</a> 930 `) 931 932 b.EditFiles("README.md", `--- 933 title: "Readme Edit" 934 --- 935 `) 936 937 b.Build(BuildCfg{}) 938 939 b.AssertFileContent("public/index.html", ` 940 Readme Edit 941 `) 942 } 943 944 func TestMountsPaths(t *testing.T) { 945 c := qt.New(t) 946 947 type test struct { 948 b *sitesBuilder 949 clean func() 950 workingDir string 951 } 952 953 prepare := func(c *qt.C, mounts string) test { 954 workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mounts-paths") 955 c.Assert(err, qt.IsNil) 956 957 configTemplate := ` 958 baseURL = "https://example.com" 959 title = "My Modular Site" 960 workingDir = %q 961 962 %s 963 964 ` 965 tomlConfig := fmt.Sprintf(configTemplate, workingDir, mounts) 966 tomlConfig = strings.Replace(tomlConfig, "WORKING_DIR", workingDir, -1) 967 968 b := newTestSitesBuilder(c).Running() 969 970 cfg := config.NewWithTestDefaults() 971 cfg.Set("workingDir", workingDir) 972 b.Fs = hugofs.NewDefault(cfg) 973 974 os.MkdirAll(filepath.Join(workingDir, "content", "blog"), 0777) 975 976 b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig) 977 978 return test{ 979 b: b, 980 clean: clean, 981 workingDir: workingDir, 982 } 983 } 984 985 c.Run("Default", func(c *qt.C) { 986 mounts := `` 987 988 test := prepare(c, mounts) 989 b := test.b 990 defer test.clean() 991 992 b.WithContent("blog/p1.md", `--- 993 title: P1 994 ---`) 995 996 b.Build(BuildCfg{}) 997 998 p := b.GetPage("blog/p1.md") 999 f := p.File().FileInfo().Meta() 1000 b.Assert(filepath.ToSlash(f.Path), qt.Equals, "blog/p1.md") 1001 b.Assert(filepath.ToSlash(f.PathFile()), qt.Equals, "content/blog/p1.md") 1002 1003 b.Assert(b.H.BaseFs.Layouts.Path(filepath.Join(test.workingDir, "layouts", "_default", "single.html")), qt.Equals, filepath.FromSlash("_default/single.html")) 1004 }) 1005 1006 c.Run("Mounts", func(c *qt.C) { 1007 absDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mounts-paths-abs") 1008 c.Assert(err, qt.IsNil) 1009 defer clean() 1010 1011 mounts := `[module] 1012 [[module.mounts]] 1013 source = "README.md" 1014 target = "content/_index.md" 1015 [[module.mounts]] 1016 source = "mycontent" 1017 target = "content/blog" 1018 [[module.mounts]] 1019 source = "subdir/mypartials" 1020 target = "layouts/partials" 1021 [[module.mounts]] 1022 source = %q 1023 target = "layouts/shortcodes" 1024 ` 1025 mounts = fmt.Sprintf(mounts, filepath.Join(absDir, "/abs/myshortcodes")) 1026 1027 test := prepare(c, mounts) 1028 b := test.b 1029 defer test.clean() 1030 1031 subContentDir := filepath.Join(test.workingDir, "mycontent", "sub") 1032 os.MkdirAll(subContentDir, 0777) 1033 myPartialsDir := filepath.Join(test.workingDir, "subdir", "mypartials") 1034 os.MkdirAll(myPartialsDir, 0777) 1035 1036 absShortcodesDir := filepath.Join(absDir, "abs", "myshortcodes") 1037 os.MkdirAll(absShortcodesDir, 0777) 1038 1039 b.WithSourceFile("README.md", "---\ntitle: Readme\n---") 1040 b.WithSourceFile("mycontent/sub/p1.md", "---\ntitle: P1\n---") 1041 1042 b.WithSourceFile(filepath.Join(absShortcodesDir, "myshort.html"), "MYSHORT") 1043 b.WithSourceFile(filepath.Join(myPartialsDir, "mypartial.html"), "MYPARTIAL") 1044 1045 b.Build(BuildCfg{}) 1046 1047 p1_1 := b.GetPage("/blog/sub/p1.md") 1048 p1_2 := b.GetPage("/mycontent/sub/p1.md") 1049 b.Assert(p1_1, qt.Not(qt.IsNil)) 1050 b.Assert(p1_2, qt.Equals, p1_1) 1051 1052 f := p1_1.File().FileInfo().Meta() 1053 b.Assert(filepath.ToSlash(f.Path), qt.Equals, "blog/sub/p1.md") 1054 b.Assert(filepath.ToSlash(f.PathFile()), qt.Equals, "mycontent/sub/p1.md") 1055 b.Assert(b.H.BaseFs.Layouts.Path(filepath.Join(myPartialsDir, "mypartial.html")), qt.Equals, filepath.FromSlash("partials/mypartial.html")) 1056 b.Assert(b.H.BaseFs.Layouts.Path(filepath.Join(absShortcodesDir, "myshort.html")), qt.Equals, filepath.FromSlash("shortcodes/myshort.html")) 1057 b.Assert(b.H.BaseFs.Content.Path(filepath.Join(subContentDir, "p1.md")), qt.Equals, filepath.FromSlash("blog/sub/p1.md")) 1058 b.Assert(b.H.BaseFs.Content.Path(filepath.Join(test.workingDir, "README.md")), qt.Equals, filepath.FromSlash("_index.md")) 1059 }) 1060 } 1061 1062 // https://github.com/gohugoio/hugo/issues/6299 1063 func TestSiteWithGoModButNoModules(t *testing.T) { 1064 t.Parallel() 1065 1066 c := qt.New(t) 1067 // We need to use the OS fs for this. 1068 workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-no-mod") 1069 c.Assert(err, qt.IsNil) 1070 1071 cfg := config.NewWithTestDefaults() 1072 cfg.Set("workingDir", workDir) 1073 fs := hugofs.NewFrom(hugofs.Os, cfg) 1074 1075 defer clean() 1076 1077 b := newTestSitesBuilder(t) 1078 b.Fs = fs 1079 1080 b.WithWorkingDir(workDir).WithViper(cfg) 1081 1082 b.WithSourceFile("go.mod", "") 1083 b.Build(BuildCfg{}) 1084 } 1085 1086 // https://github.com/gohugoio/hugo/issues/6622 1087 func TestModuleAbsMount(t *testing.T) { 1088 t.Parallel() 1089 1090 c := qt.New(t) 1091 // We need to use the OS fs for this. 1092 workDir, clean1, err := htesting.CreateTempDir(hugofs.Os, "hugo-project") 1093 c.Assert(err, qt.IsNil) 1094 absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content") 1095 c.Assert(err, qt.IsNil) 1096 1097 cfg := config.NewWithTestDefaults() 1098 cfg.Set("workingDir", workDir) 1099 fs := hugofs.NewFrom(hugofs.Os, cfg) 1100 1101 config := fmt.Sprintf(` 1102 workingDir=%q 1103 1104 [module] 1105 [[module.mounts]] 1106 source = %q 1107 target = "content" 1108 1109 `, workDir, absContentDir) 1110 1111 defer clean1() 1112 defer clean2() 1113 1114 b := newTestSitesBuilder(t) 1115 b.Fs = fs 1116 1117 contentFilename := filepath.Join(absContentDir, "p1.md") 1118 afero.WriteFile(hugofs.Os, contentFilename, []byte(` 1119 --- 1120 title: Abs 1121 --- 1122 1123 Content. 1124 `), 0777) 1125 1126 b.WithWorkingDir(workDir).WithConfigFile("toml", config) 1127 b.WithContent("dummy.md", "") 1128 1129 b.WithTemplatesAdded("index.html", ` 1130 {{ $p1 := site.GetPage "p1" }} 1131 P1: {{ $p1.Title }}|{{ $p1.RelPermalink }}|Filename: {{ $p1.File.Filename }} 1132 `) 1133 1134 b.Build(BuildCfg{}) 1135 1136 b.AssertFileContent("public/index.html", "P1: Abs|/p1/", "Filename: "+contentFilename) 1137 } 1138 1139 // Issue 9426 1140 func TestMountSameSource(t *testing.T) { 1141 config := `baseURL = 'https://example.org/' 1142 languageCode = 'en-us' 1143 title = 'Hugo GitHub Issue #9426' 1144 1145 disableKinds = ['RSS','sitemap','taxonomy','term'] 1146 1147 [[module.mounts]] 1148 source = "content" 1149 target = "content" 1150 1151 [[module.mounts]] 1152 source = "extra-content" 1153 target = "content/resources-a" 1154 1155 [[module.mounts]] 1156 source = "extra-content" 1157 target = "content/resources-b" 1158 ` 1159 b := newTestSitesBuilder(t).WithConfigFile("toml", config) 1160 1161 b.WithContent("p1.md", "") 1162 1163 b.WithSourceFile( 1164 "extra-content/_index.md", "", 1165 "extra-content/subdir/_index.md", "", 1166 "extra-content/subdir/about.md", "", 1167 ) 1168 1169 b.Build(BuildCfg{}) 1170 1171 b.AssertFileContent("public/resources-a/subdir/about/index.html", "Single") 1172 b.AssertFileContent("public/resources-b/subdir/about/index.html", "Single") 1173 }