diff --git a/app/models/theme.rb b/app/models/theme.rb
index 923415ecf1d..e1cc0419be7 100644
--- a/app/models/theme.rb
+++ b/app/models/theme.rb
@@ -151,9 +151,6 @@ class Theme < ActiveRecord::Base
     end
 
     Theme.expire_site_cache!
-    ColorScheme.hex_cache.clear
-    CSP::Extension.clear_theme_extensions_cache!
-    SvgSprite.expire_cache
   end
 
   def self.compiler_version
@@ -217,6 +214,8 @@ class Theme < ActiveRecord::Base
     clear_cache!
     ApplicationSerializer.expire_cache_fragment!("user_themes")
     ColorScheme.hex_cache.clear
+    CSP::Extension.clear_theme_extensions_cache!
+    SvgSprite.expire_cache
   end
 
   def self.clear_default!
diff --git a/lib/discourse.rb b/lib/discourse.rb
index 757eeabf325..4616b887d2d 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -1028,4 +1028,14 @@ module Discourse
   def self.allow_dev_populate?
     Rails.env.development? || ENV["ALLOW_DEV_POPULATE"] == "1"
   end
+
+  # warning: this method is very expensive and shouldn't be called in places
+  # where performance matters. it's meant to be called manually (e.g. in the
+  # rails console) when dealing with an emergency that requires invalidating
+  # theme cache
+  def self.clear_all_theme_cache!
+    ThemeField.force_recompilation!
+    Theme.all.each(&:update_javascript_cache!)
+    Theme.expire_site_cache!
+  end
 end
diff --git a/spec/lib/discourse_spec.rb b/spec/lib/discourse_spec.rb
index b419814331c..cd3a85e6520 100644
--- a/spec/lib/discourse_spec.rb
+++ b/spec/lib/discourse_spec.rb
@@ -472,4 +472,128 @@ describe Discourse do
     end
   end
 
+  context ".clear_all_theme_cache!" do
+    before do
+      setup_s3
+      SiteSetting.s3_cdn_url = "https://s3.cdn.com/gg"
+      stub_s3_store
+    end
+
+    let!(:theme) { Fabricate(:theme) }
+    let!(:upload) { Fabricate(:s3_image_upload) }
+    let!(:upload_theme_field) do
+      Fabricate(
+        :theme_field,
+        theme: theme,
+        upload: upload,
+        type_id: ThemeField.types[:theme_upload_var],
+        target_id: Theme.targets[:common],
+        name: "imajee",
+        value: "",
+      )
+    end
+    let!(:basic_html_field) do
+      Fabricate(
+        :theme_field,
+        theme: theme,
+        type_id: ThemeField.types[:html],
+        target_id: Theme.targets[:common],
+        name: "head_tag",
+        value: <<~HTML
+          <script type="text/discourse-plugin" version="0.1">
+            console.log(settings.uploads.imajee);
+          </script>
+        HTML
+      )
+    end
+    let!(:js_field) do
+      Fabricate(
+        :theme_field,
+        theme: theme,
+        type_id: ThemeField.types[:js],
+        target_id: Theme.targets[:extra_js],
+        name: "somefile.js",
+        value: <<~JS
+          console.log(settings.uploads.imajee);
+        JS
+      )
+    end
+    let!(:scss_field) do
+      Fabricate(
+        :theme_field,
+        theme: theme,
+        type_id: ThemeField.types[:scss],
+        target_id: Theme.targets[:common],
+        name: "scss",
+        value: <<~SCSS
+          .something { background: url($imajee); }
+        SCSS
+      )
+    end
+
+    it "invalidates all JS and CSS caches" do
+      Stylesheet::Manager.clear_theme_cache!
+
+      old_upload_url = Discourse.store.cdn_url(upload.url)
+
+      head_tag_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :desktop, "head_tag")
+      ).css('script').first
+      head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content
+      expect(head_tag_js).to include(old_upload_url)
+
+      js_file_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :extra_js, nil)
+      ).css('script').first
+      file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content
+      expect(file_js).to include(old_upload_url)
+
+      css_link_tag = Nokogiri::HTML5.fragment(
+        Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all')
+      ).css('link').first
+      css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content
+      expect(css).to include("url(#{old_upload_url})")
+
+      SiteSetting.s3_cdn_url = "https://new.s3.cdn.com/gg"
+      new_upload_url = Discourse.store.cdn_url(upload.url)
+
+      head_tag_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :desktop, "head_tag")
+      ).css('script').first
+      head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content
+      expect(head_tag_js).to include(old_upload_url)
+
+      js_file_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :extra_js, nil)
+      ).css('script').first
+      file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content
+      expect(file_js).to include(old_upload_url)
+
+      css_link_tag = Nokogiri::HTML5.fragment(
+        Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all')
+      ).css('link').first
+      css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content
+      expect(css).to include("url(#{old_upload_url})")
+
+      Discourse.clear_all_theme_cache!
+
+      head_tag_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :desktop, "head_tag")
+      ).css('script').first
+      head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content
+      expect(head_tag_js).to include(new_upload_url)
+
+      js_file_script = Nokogiri::HTML5.fragment(
+        Theme.lookup_field(theme.id, :extra_js, nil)
+      ).css('script').first
+      file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content
+      expect(file_js).to include(new_upload_url)
+
+      css_link_tag = Nokogiri::HTML5.fragment(
+        Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all')
+      ).css('link').first
+      css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content
+      expect(css).to include("url(#{new_upload_url})")
+    end
+  end
 end