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 }