diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index ef58f531f..de58d5b7a 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -11,6 +11,8 @@ - `sql_table` now alternatively takes an array of constraints instead of being limited to a single one. Thanks @satoqz ! [#1245](https://github.com/terrastruct/d2/pull/1245) - Constraints in `sql_table` render even if they have no matching abbreviation [#1372](https://github.com/terrastruct/d2/pull/1372) - Constraints in `sql_table` sheds their excessive letter-spacing and is padded from the end consistently [#1372](https://github.com/terrastruct/d2/pull/1372) +- Duplicate image URLs in icons are only fetched once [#1373](https://github.com/terrastruct/d2/pull/1373) +- In watch mode, images are cached by default across compiles. Can be disabled with flag `--img-cache=0`. [#1373](https://github.com/terrastruct/d2/pull/1373) #### Bugfixes ⛑️ diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index 81bc4e895..ff8263105 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -109,6 +109,9 @@ An appendix for tooltips and links is added to PNG exports since they are not in .Ns . .It Fl d , -debug Print debug logs. +.It Fl -img-cache Ar true +In watch mode, images used in icons are cached for subsequent compilations. This should be disabled if images might change +.Ns . .It Fl h , -help Print usage information and exit. .It Fl v , -version diff --git a/d2cli/main.go b/d2cli/main.go index 0db873bc6..9b5f1cc8a 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -67,6 +67,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } + imgCacheFlag, err := ms.Opts.Bool("IMG_CACHE", "img-cache", "", true, "in watch mode, images used in icons are cached for subsequent compilations. This should be disabled if images might change.") + if err != nil { + return err + } layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used`) themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID") if err != nil { @@ -150,6 +154,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if *debugFlag { ms.Env.Setenv("DEBUG", "1") } + if *imgCacheFlag { + ms.Env.Setenv("IMG_CACHE", "1") + } if *browserFlag != "" { ms.Env.Setenv("BROWSER", *browserFlag) } diff --git a/lib/imgbundler/imgbundler.go b/lib/imgbundler/imgbundler.go index 5c6de7038..526f3bf8d 100644 --- a/lib/imgbundler/imgbundler.go +++ b/lib/imgbundler/imgbundler.go @@ -23,6 +23,8 @@ import ( "oss.terrastruct.com/util-go/xmain" ) +var imgCache sync.Map + const maxImageSize int64 = 1 << 25 // 33_554_432 var imageRegex = regexp.MustCompile(`ab +`, url1, url2) + + ms := &xmain.State{ + Name: "test", + + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + + Env: xos.NewEnv(os.Environ()), + } + ms.Log = cmdlog.NewTB(ms.Env, t) + + count := 0 + + httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response { + count++ + respRecorder := httptest.NewRecorder() + respRecorder.WriteString(`\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n`) + respRecorder.WriteHeader(200) + return respRecorder.Result() + }) + + out, err := BundleRemote(ctx, ms, []byte(sampleSVG)) + if err != nil { + t.Fatal(err) + } + tassert.Equal(t, 1, count) + if strings.Contains(string(out), url1) { + t.Fatal("links still exist") + } + tassert.Equal(t, 2, strings.Count(string(out), "image/svg+xml")) +} + +func TestImgCache(t *testing.T) { + imgCache = sync.Map{} + ctx := context.Background() + url1 := "https://icons.terrastruct.com/essentials/004-picture.svg" + url2 := "https://icons.terrastruct.com/essentials/004-picture.svg" + + sampleSVG := fmt.Sprintf(` +ab +`, url1, url2) + + ms := &xmain.State{ + Name: "test", + + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + + Env: xos.NewEnv(os.Environ()), + } + ms.Log = cmdlog.NewTB(ms.Env, t) + + count := 0 + + httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response { + count++ + respRecorder := httptest.NewRecorder() + respRecorder.WriteString(`\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n`) + respRecorder.WriteHeader(200) + return respRecorder.Result() + }) + + // Using a cache, imgs are not refetched on multiple runs + ms.Env.Setenv("IMG_CACHE", "1") + _, err := BundleRemote(ctx, ms, []byte(sampleSVG)) + if err != nil { + t.Fatal(err) + } + _, err = BundleRemote(ctx, ms, []byte(sampleSVG)) + if err != nil { + t.Fatal(err) + } + tassert.Equal(t, 1, count) + + // With cache disabled, it refetches + ms.Env.Setenv("IMG_CACHE", "0") + count = 0 + _, err = BundleRemote(ctx, ms, []byte(sampleSVG)) + if err != nil { + t.Fatal(err) + } + _, err = BundleRemote(ctx, ms, []byte(sampleSVG)) + if err != nil { + t.Fatal(err) + } + tassert.Equal(t, 2, count) +}