diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..79ae2a1 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,22 @@ +name: Go + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 'stable' + + - name: Test + run: go test -v . diff --git a/fragment.go b/fragment.go index 9873525..23c8a11 100644 --- a/fragment.go +++ b/fragment.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "html/template" + "net/url" ) // Fragment holds HTML content generated for Vite integration, intended to be @@ -104,8 +105,14 @@ func HTMLFragment(config Config) (*Fragment, error) { // Create a buffer to store the executed template output var buf bytes.Buffer + // Pass the JoinPath function to the template so we + // can use {{ urljoin .base .path }} + templateFuncs := template.FuncMap{ + "urljoin": url.JoinPath, + } + // Parse the predefined headTmpl into a new template - tmpl, err := template.New("vite").Parse(htmlTmpl) + tmpl, err := template.New("vite").Funcs(templateFuncs).Parse(htmlTmpl) if err != nil { // Return an error if parsing fails return nil, fmt.Errorf("vite: parse template: %w", err) @@ -128,11 +135,11 @@ func HTMLFragment(config Config) (*Fragment, error) { const htmlTmpl = ` {{- if .IsDev }} {{ .PluginReactPreamble }} - + {{- if ne .ViteEntry "" }} - + {{- else }} - + {{- end }} {{- else }} {{- if .StyleSheets }} diff --git a/helper_function_test.go b/helper_function_test.go new file mode 100644 index 0000000..eaf8868 --- /dev/null +++ b/helper_function_test.go @@ -0,0 +1,209 @@ +package vite_test + +import ( + "fmt" + "io/fs" + "strings" + "testing" + "testing/fstest" + + "github.com/olivere/vite" +) + +// from https://github.com/vitejs/vite/blob/242f550eb46c93896fca6b55495578921e29a8af/docs/guide/backend-integration.md +const exampleManifest string = ` +{ + "_shared-CPdiUi_T.js": { + "file": "assets/shared-ChJ_j-JJ.css", + "src": "_shared-CPdiUi_T.js" + }, + "_shared-B7PI925R.js": { + "file": "assets/shared-B7PI925R.js", + "name": "shared", + "css": ["assets/shared-ChJ_j-JJ.css"] + }, + "baz.js": { + "file": "assets/baz-B2H3sXNv.js", + "name": "baz", + "src": "baz.js", + "isDynamicEntry": true + }, + "views/bar.js": { + "file": "assets/bar-gkvgaI9m.js", + "name": "bar", + "src": "views/bar.js", + "isEntry": true, + "imports": ["_shared-B7PI925R.js"], + "dynamicImports": ["baz.js"] + }, + "views/foo.js": { + "file": "assets/foo-BRBmoGS9.js", + "name": "foo", + "src": "views/foo.js", + "isEntry": true, + "imports": ["_shared-B7PI925R.js"], + "css": ["assets/foo-5UjPuW-k.css"] + } +} +` + +// these are the tags we should be generating based on the manifest +const fooEntrpointTagsBlock string = ` + + + + +` + +const barEntrypointTagsBlock string = ` + + + +` + +func getTestFS() fs.FS { + manifestFile := fstest.MapFile{ + Data: []byte(exampleManifest), + } + return fstest.MapFS{ + ".vite/manifest.json": &manifestFile, + } +} + +func TestFragmentContainsTagsForFooEntrpointFromManifest(t *testing.T) { + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: getTestFS(), + IsDev: false, + ViteEntry: "views/foo.js", + }) + + if err != nil { + t.Fatal("Unable to produce Vite HTML Fragment", err) + } + + generatedHTML := string(viteFragment.Tags) + + fooEntrypointTags := strings.Split(fooEntrpointTagsBlock, "\n") + + for _, tag := range fooEntrypointTags { + if tag == "" { + continue + } + + HTMLContainsTag := strings.Contains(generatedHTML, strings.TrimSpace(tag)) + if !HTMLContainsTag { + t.Logf(` + ------------ Generated HTML: --- %s + `, generatedHTML) + t.Fatalf("Generated HTML block does not contain needed tag: %s", tag) + } + } +} + +func TestFragmentContainsTagsForBarEntrpointFromManifest(t *testing.T) { + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: getTestFS(), + IsDev: false, + ViteEntry: "views/bar.js", + }) + + if err != nil { + t.Fatal("Unable to produce Vite HTML Fragment", err) + } + + generatedHTML := string(viteFragment.Tags) + + barEntrypointTags := strings.Split(barEntrypointTagsBlock, "\n") + + for _, tag := range barEntrypointTags { + if tag == "" { + continue + } + + HTMLContainsTag := strings.Contains(generatedHTML, strings.TrimSpace(tag)) + if !HTMLContainsTag { + t.Logf(` + ------------ Generated HTML: --- %s + `, generatedHTML) + t.Fatalf("Generated HTML block does not contain needed tag: %s", tag) + + } + } +} + +func TestDevModeFragmentContainsModuleTags(t *testing.T) { + const entrypoint string = "/main.js" + + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: getTestFS(), + IsDev: true, + ViteURL: "http://localhost:5173", + ViteEntry: entrypoint, + }) + + if err != nil { + t.Fatal("Unable to produce Vite HTML Fragment", err) + } + + generatedHTML := string(viteFragment.Tags) + + const viteClientTag string = `` + var entrypointTag string = fmt.Sprintf(``, entrypoint) + + if !strings.Contains(generatedHTML, viteClientTag) { + t.Fatalf("Generated HTML block does not contain: %s", viteClientTag) + } + + if !strings.Contains(generatedHTML, entrypointTag) { + t.Fatalf("Generated HTML block does not contain: %s", entrypointTag) + } +} + +func TestDevModeFragmentContainsModuleTagsWithoutEntrypointSet(t *testing.T) { + + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: getTestFS(), + IsDev: true, + ViteURL: "http://localhost:5173", + }) + + if err != nil { + t.Fatal("Unable to produce Vite HTML Fragment", err) + } + + generatedHTML := string(viteFragment.Tags) + + const viteClientTag string = `` + const entrypointTag string = `` + + if !strings.Contains(generatedHTML, viteClientTag) { + t.Fatalf("Generated HTML block does not contain: %s", viteClientTag) + } + + if !strings.Contains(generatedHTML, entrypointTag) { + t.Fatalf("Generated HTML block does not contain: %s", entrypointTag) + } +} + +func TestDevModeFragmentWorksWithTrailingSlash(t *testing.T) { + const entrypoint string = "main.js" + + viteFragment, err := vite.HTMLFragment(vite.Config{ + FS: getTestFS(), + IsDev: true, + ViteURL: "http://localhost:5173/", + ViteEntry: entrypoint, + }) + + if err != nil { + t.Fatal("Unable to produce Vite HTML Fragment", err) + } + + generatedHTML := string(viteFragment.Tags) + + const viteClientTag string = `` + + if !strings.Contains(generatedHTML, viteClientTag) { + t.Fatalf("Generated HTML block does not contain: %s", viteClientTag) + } +} diff --git a/manifest.go b/manifest.go index 96c0092..e8a327c 100644 --- a/manifest.go +++ b/manifest.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "net/url" "strings" ) @@ -69,13 +70,14 @@ func (m Manifest) GetChunk(name string) (*Chunk, bool) { // PluginReactPreamble returns the script tag that should be injected into the // HTML to enable React Fast Refresh. func PluginReactPreamble(server string) string { + url, _ := url.JoinPath(server, "/@react-refresh") return fmt.Sprintf(``, server) +`, url) } // GenerateCSS generates the CSS links for the given chunk. @@ -154,10 +156,10 @@ func (m Manifest) GeneratePreloadModules(name string) string { } if chunk.File != "" { - sb.WriteString(``) + sb.WriteString(`">`) } for _, imp := range chunk.Imports {