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 {