FEATURE: Multiple SCSS file support for themes (#7351)

Theme developers can include any number of scss files within the /scss/ directory of a theme. These can then be imported from the main common/desktop/mobile scss.
This commit is contained in:
David Taylor
2019-04-12 11:36:08 +01:00
committed by GitHub
parent 0e9a0a31f5
commit 268d4d4c82
19 changed files with 302 additions and 125 deletions

View File

@@ -59,4 +59,52 @@ describe Stylesheet::Importer do
end
context "extra_scss" do
let(:scss) { "body { background: red}" }
let(:theme) { Fabricate(:theme).tap { |t|
t.set_field(target: :extra_scss, name: "my_files/magic", value: scss)
t.save!
}}
let(:importer) { described_class.new(theme: theme) }
it "should be able to import correctly" do
# Import from regular theme file
expect(
importer.imports(
"my_files/magic",
"theme_#{theme.id}/desktop-scss-mytheme.scss"
).source).to eq(scss)
# Import from some deep file
expect(
importer.imports(
"my_files/magic",
"theme_#{theme.id}/some/deep/folder/structure/myfile.scss"
).source).to eq(scss)
# Import from parent dir
expect(
importer.imports(
"../../my_files/magic",
"theme_#{theme.id}/my_files/folder1/myfile.scss"
).source).to eq(scss)
# Import from same dir without ./
expect(
importer.imports(
"magic",
"theme_#{theme.id}/my_files/myfile.scss"
).source).to eq(scss)
# Import from same dir with ./
expect(
importer.imports(
"./magic",
"theme_#{theme.id}/my_files/myfile.scss"
).source).to eq(scss)
end
end
end

View File

@@ -73,19 +73,23 @@ describe SvgSprite do
# Works when applying override
theme.update_setting(:custom_icon, "gas-pump")
theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("gas-pump")
# Works when changing override
theme.update_setting(:custom_icon, "gamepad")
theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("gamepad")
expect(SvgSprite.all_icons([theme.id])).not_to include("gas-pump")
# FA5 syntax
theme.update_setting(:custom_icon, "fab fa-bandcamp")
theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("fab-bandcamp")
# Internal Discourse syntax + multiple icons
theme.update_setting(:custom_icon, "fab-android|dragon")
theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("fab-android")
expect(SvgSprite.all_icons([theme.id])).to include("dragon")
@@ -94,6 +98,7 @@ describe SvgSprite do
# Check components are included
theme.update(component: true)
theme.save!
parent_theme = Fabricate(:theme)
parent_theme.add_child_theme!(theme)
expect(SvgSprite.all_icons([parent_theme.id])).to include("dragon")

View File

@@ -108,7 +108,7 @@ describe ThemeStore::TgzExporter do
# but protection is in place 'just in case'
expect do
theme.set_field(target: :translations, name: "en", value: "hacked")
theme.theme_fields[0].stubs(:file_path).returns("../../malicious")
ThemeField.any_instance.stubs(:file_path).returns("../../malicious")
theme.save!
package
end.to raise_error(RuntimeError)

View File

@@ -1,15 +0,0 @@
require 'rails_helper'
describe Jobs::RebakeAllHtmlThemeFields do
let(:theme) { Fabricate(:theme) }
let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "<script>console.log(123)</script>") }
it 'extracts inline javascripts' do
theme_field.update_attributes(value_baked: 'need to be rebaked')
described_class.new.execute_onceoff({})
theme_field.reload
expect(theme_field.value_baked).to include('theme-javascripts')
end
end

View File

@@ -9,7 +9,7 @@ describe RemoteTheme do
`cd #{repo_dir} && git init . `
`cd #{repo_dir} && git config user.email 'someone@cool.com'`
`cd #{repo_dir} && git config user.name 'The Cool One'`
`cd #{repo_dir} && mkdir desktop mobile common assets locales`
`cd #{repo_dir} && mkdir desktop mobile common assets locales scss`
files.each do |name, data|
File.write("#{repo_dir}/#{name}", data)
`cd #{repo_dir} && git add #{name}`
@@ -46,6 +46,7 @@ describe RemoteTheme do
setup_git_repo(
"about.json" => about_json,
"desktop/desktop.scss" => scss_data,
"scss/file.scss" => ".class1{color:red}",
"common/header.html" => "I AM HEADER",
"common/random.html" => "I AM SILLY",
"common/embedded.scss" => "EMBED",
@@ -77,7 +78,7 @@ describe RemoteTheme do
expect(remote.theme_version).to eq("1.0")
expect(remote.minimum_discourse_version).to eq("1.0.0")
expect(@theme.theme_fields.length).to eq(6)
expect(@theme.theme_fields.length).to eq(7)
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
@@ -91,7 +92,7 @@ describe RemoteTheme do
expect(mapped["4-en"]).to eq("sometranslations")
expect(mapped.length).to eq(6)
expect(mapped.length).to eq(7)
expect(@theme.settings.length).to eq(1)
expect(@theme.settings.first.value).to eq(true)
@@ -112,6 +113,8 @@ describe RemoteTheme do
`cd #{initial_repo} && git add settings.yml`
File.delete("#{initial_repo}/settings.yaml")
File.delete("#{initial_repo}/scss/file.scss")
`cd #{initial_repo} && git commit -am "update"`
time = Time.new('2001')
@@ -122,7 +125,7 @@ describe RemoteTheme do
expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip)
remote.update_from_remote
@theme.save
@theme.save!
@theme.reload
scheme = ColorScheme.find_by(theme_id: @theme.id)
@@ -132,6 +135,9 @@ describe RemoteTheme do
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
# Scss file was deleted
expect(mapped["5-file"]).to eq(nil)
expect(mapped["0-header"]).to eq("I AM UPDATED")
expect(mapped["1-scss"]).to eq(scss_data)

View File

@@ -29,6 +29,7 @@ describe ThemeField do
it 'does not insert a script tag when there are no inline script' do
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "body_tag", value: '<div>new div</div>')
theme_field.ensure_baked!
expect(theme_field.value_baked).to_not include('<script')
end
@@ -53,7 +54,7 @@ describe ThemeField do
HTML
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
theme_field.ensure_baked!
expect(theme_field.value_baked).to include("<script src=\"#{theme_field.javascript_cache.url}\"></script>")
expect(theme_field.value_baked).to include("external-script.js")
expect(theme_field.value_baked).to include('<script type="text/template"')
@@ -75,7 +76,7 @@ describe ThemeField do
JavaScript
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
theme_field.ensure_baked!
expect(theme_field.javascript_cache.content).to include(extracted)
end
@@ -87,11 +88,13 @@ describe ThemeField do
HTML
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
field.ensure_baked!
expect(field.error).not_to eq(nil)
expect(field.value_baked).to include("<script src=\"#{field.javascript_cache.url}\"></script>")
expect(field.javascript_cache.content).to include("Theme Transpilation Error:")
field.update!(value: '')
field.ensure_baked!
expect(field.error).to eq(nil)
end
@@ -102,8 +105,9 @@ HTML
</script>
HTML
ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: "string_setting: \"test text \\\" 123!\"")
ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: "string_setting: \"test text \\\" 123!\"").ensure_baked!
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "head_tag", value: html)
theme_field.ensure_baked!
javascript_cache = theme_field.javascript_cache
expect(theme_field.value_baked).to include("<script src=\"#{javascript_cache.url}\"></script>")
@@ -115,15 +119,33 @@ HTML
it "correctly generates errors for transpiled css" do
css = "body {"
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "scss", value: css)
field.reload
field.ensure_baked!
expect(field.error).not_to eq(nil)
field.value = "body {color: blue};"
field.save!
field.reload
field.ensure_baked!
expect(field.error).to eq(nil)
end
it "allows importing scss files" do
theme = Fabricate(:theme)
main_field = theme.set_field(target: :common, name: :scss, value: ".class1{color: red}\n@import 'rootfile1';")
theme.set_field(target: :extra_scss, name: "rootfile1", value: ".class2{color:green}\n@import 'foldername/subfile1';")
theme.set_field(target: :extra_scss, name: "rootfile2", value: ".class3{color:green} ")
theme.set_field(target: :extra_scss, name: "foldername/subfile1", value: ".class4{color:yellow}\n@import 'subfile2';")
theme.set_field(target: :extra_scss, name: "foldername/subfile2", value: ".class5{color:yellow}\n@import '../rootfile2';")
theme.save!
result = main_field.compile_scss[0]
expect(result).to include(".class1")
expect(result).to include(".class2")
expect(result).to include(".class3")
expect(result).to include(".class4")
expect(result).to include(".class5")
end
def create_upload_theme_field!(name)
ThemeField.create!(
theme_id: 1,
@@ -131,7 +153,7 @@ HTML
value: "",
type_id: ThemeField.types[:theme_upload_var],
name: name,
)
).tap { |tf| tf.ensure_baked! }
end
it "ensures we don't use invalid SCSS variable names" do
@@ -145,7 +167,7 @@ HTML
def create_yaml_field(value)
field = ThemeField.create!(theme_id: 1, target_id: Theme.targets[:settings], name: "yaml", value: value)
field.reload
field.ensure_baked!
field
end
@@ -179,7 +201,7 @@ HTML
field.value = "valid_setting: true"
field.save!
field.reload
field.ensure_baked!
expect(field.error).to eq(nil)
end
@@ -319,6 +341,7 @@ HTML
HTML
theme_field = ThemeField.create!(theme_id: theme.id, target_id: 0, name: "head_tag", value: html)
theme_field.ensure_baked!
javascript_cache = theme_field.javascript_cache
expect(javascript_cache.content).to include("inline discourse plugin")
expect(javascript_cache.content).to include("theme_translations.#{theme.id}.")

View File

@@ -238,6 +238,7 @@ HTML
context "plugin api" do
def transpile(html)
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
f.ensure_baked!
return f.value_baked, f.javascript_cache
end
@@ -307,6 +308,7 @@ HTML
setting = theme.settings.find { |s| s.name == :font_size }
setting.value = '30px'
theme.save!
scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
expect(scss).to include("font-size:30px")
@@ -356,6 +358,7 @@ HTML
setting = theme.settings.find { |s| s.name == :name }
setting.value = 'bill'
theme.save!
transpiled = <<~HTML
(function() {
@@ -428,6 +431,7 @@ HTML
expect(user_themes).to eq([])
theme = Fabricate(:theme, name: "bob", user_selectable: true)
theme.save!
json = Site.json_for(guardian)
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
@@ -485,6 +489,7 @@ HTML
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":true/)
theme.settings.first.value = "false"
theme.save!
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":false/)
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")