Support persisting unminified external stylesheets (#33919)

* Support local tweaking of external stylesheets

Signed-off-by: Taym <haddadi.taym@gmail.com>

* Remove duplicated code between unminify_css and unminify_js

Signed-off-by: Taym <haddadi.taym@gmail.com>

* Add License

Signed-off-by: Taym <haddadi.taym@gmail.com>

* Use js-beautify instead of npx

Signed-off-by: Taym <haddadi.taym@gmail.com>

* Fix clippy warning

Signed-off-by: Taym <haddadi.taym@gmail.com>

---------

Signed-off-by: Taym <haddadi.taym@gmail.com>
This commit is contained in:
Taym Haddadi 2024-10-30 12:12:20 +01:00 committed by GitHub
parent bac1101163
commit ee68dc2589
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 206 additions and 77 deletions

1
.gitignore vendored
View file

@ -57,6 +57,7 @@ Session.vim
Sessionx.vim Sessionx.vim
/unminified-js /unminified-js
/unminified-css
# Layout debugger trace files # Layout debugger trace files
layout_trace* layout_trace*

View file

@ -126,6 +126,9 @@ pub struct Opts {
/// Directory path that was created with "unminify-js" /// Directory path that was created with "unminify-js"
pub local_script_source: Option<String>, pub local_script_source: Option<String>,
/// Unminify Css.
pub unminify_css: bool,
/// Print Progressive Web Metrics to console. /// Print Progressive Web Metrics to console.
pub print_pwm: bool, pub print_pwm: bool,
} }
@ -421,6 +424,7 @@ pub fn default_opts() -> Opts {
ignore_certificate_errors: false, ignore_certificate_errors: false,
unminify_js: false, unminify_js: false,
local_script_source: None, local_script_source: None,
unminify_css: false,
print_pwm: false, print_pwm: false,
} }
} }
@ -560,6 +564,7 @@ pub fn from_cmdline_args(mut opts: Options, args: &[String]) -> ArgumentParsingR
"Directory root with unminified scripts", "Directory root with unminified scripts",
"", "",
); );
opts.optflag("", "unminify-css", "Unminify Css");
let opt_match = match opts.parse(args) { let opt_match = match opts.parse(args) {
Ok(m) => m, Ok(m) => m,
@ -761,6 +766,7 @@ pub fn from_cmdline_args(mut opts: Options, args: &[String]) -> ArgumentParsingR
ignore_certificate_errors: opt_match.opt_present("ignore-certificate-errors"), ignore_certificate_errors: opt_match.opt_present("ignore-certificate-errors"),
unminify_js: opt_match.opt_present("unminify-js"), unminify_js: opt_match.opt_present("unminify-js"),
local_script_source: opt_match.opt_str("local-script-source"), local_script_source: opt_match.opt_str("local-script-source"),
unminify_css: opt_match.opt_present("unminify-css"),
print_pwm: opt_match.opt_present("print-pwm"), print_pwm: opt_match.opt_present("print-pwm"),
}; };

View file

@ -70,6 +70,9 @@ use crate::script_runtime::CanGc;
use crate::task::TaskCanceller; use crate::task::TaskCanceller;
use crate::task_source::dom_manipulation::DOMManipulationTaskSource; use crate::task_source::dom_manipulation::DOMManipulationTaskSource;
use crate::task_source::{TaskSource, TaskSourceName}; use crate::task_source::{TaskSource, TaskSourceName};
use crate::unminify::{
create_output_file, create_temp_files, execute_js_beautify, BeautifyFileType,
};
// TODO Implement offthread compilation in mozjs // TODO Implement offthread compilation in mozjs
/*pub struct OffThreadCompilationContext { /*pub struct OffThreadCompilationContext {
@ -853,17 +856,11 @@ impl HTMLScriptElement {
} }
fn unminify_js(&self, script: &mut ScriptOrigin) { fn unminify_js(&self, script: &mut ScriptOrigin) {
if !self.parser_document.window().unminify_js() { if self.parser_document.window().unminified_js_dir().is_none() {
return; return;
} }
// Write the minified code to a temporary file and pass its path as an argument if let Some((mut input, mut output)) = create_temp_files() {
// to js-beautify to read from. Meanwhile, redirect the process' stdout into
// another temporary file and read that into a string. This avoids some hangs
// observed on macOS when using direct input/output pipes with very large
// unminified content.
let (input, output) = (tempfile::NamedTempFile::new(), tempfile::tempfile());
if let (Ok(mut input), Ok(mut output)) = (input, output) {
match &script.code { match &script.code {
SourceCode::Text(text) => { SourceCode::Text(text) => {
input.write_all(text.as_bytes()).unwrap(); input.write_all(text.as_bytes()).unwrap();
@ -874,66 +871,24 @@ impl HTMLScriptElement {
.unwrap(); .unwrap();
}, },
} }
match Command::new("js-beautify")
.arg(input.path()) if execute_js_beautify(
.stdout(output.try_clone().unwrap()) input.path(),
.status() output.try_clone().unwrap(),
{ BeautifyFileType::Js,
Ok(status) if status.success() => { ) {
let mut script_content = String::new(); let mut script_content = String::new();
output.seek(std::io::SeekFrom::Start(0)).unwrap(); output.seek(std::io::SeekFrom::Start(0)).unwrap();
output.read_to_string(&mut script_content).unwrap(); output.read_to_string(&mut script_content).unwrap();
script.code = SourceCode::Text(Rc::new(DOMString::from(script_content))); script.code = SourceCode::Text(Rc::new(DOMString::from(script_content)));
},
_ => {
warn!("Failed to execute js-beautify. Will store unmodified script");
},
} }
} else {
warn!("Error creating input and output files for unminify");
} }
let path = match window_from_node(self).unminified_js_dir() { match create_output_file(
Some(unminified_js_dir) => PathBuf::from(unminified_js_dir), window_from_node(self).unminified_js_dir(),
None => { &script.url,
warn!("Unminified script directory not found"); Some(script.external),
return; ) {
},
};
let (base, has_name) = match script.url.as_str().ends_with('/') {
true => (
path.join(&script.url[url::Position::BeforeHost..])
.as_path()
.to_owned(),
false,
),
false => (
path.join(&script.url[url::Position::BeforeHost..])
.parent()
.unwrap()
.to_owned(),
true,
),
};
match create_dir_all(base.clone()) {
Ok(()) => debug!("Created base dir: {:?}", base),
Err(e) => {
debug!("Failed to create base dir: {:?}, {:?}", base, e);
return;
},
}
let path = if script.external && has_name {
// External script.
path.join(&script.url[url::Position::BeforeHost..])
} else {
// Inline script or url ends with '/'
base.join(Uuid::new_v4().to_string())
};
debug!("script will be stored in {:?}", path);
match File::create(&path) {
Ok(mut file) => match &script.code { Ok(mut file) => match &script.code {
SourceCode::Text(text) => file.write_all(text.as_bytes()).unwrap(), SourceCode::Text(text) => file.write_all(text.as_bytes()).unwrap(),
SourceCode::Compiled(compiled_source_code) => { SourceCode::Compiled(compiled_source_code) => {

View file

@ -297,6 +297,10 @@ pub struct Window {
/// opt is enabled. /// opt is enabled.
unminified_js_dir: DomRefCell<Option<String>>, unminified_js_dir: DomRefCell<Option<String>>,
/// Directory to store unminified css for this window if unminify-css
/// opt is enabled.
unminified_css_dir: DomRefCell<Option<String>>,
/// Directory with stored unminified scripts /// Directory with stored unminified scripts
local_script_source: Option<String>, local_script_source: Option<String>,
@ -330,6 +334,9 @@ pub struct Window {
/// Unminify Javascript. /// Unminify Javascript.
unminify_js: bool, unminify_js: bool,
/// Unminify Css.
unminify_css: bool,
/// Where to load userscripts from, if any. An empty string will load from /// Where to load userscripts from, if any. An empty string will load from
/// the resources/user-agent-js directory, and if the option isn't passed userscripts /// the resources/user-agent-js directory, and if the option isn't passed userscripts
/// won't be loaded. /// won't be loaded.
@ -538,10 +545,6 @@ impl Window {
self.replace_surrogates self.replace_surrogates
} }
pub fn unminify_js(&self) -> bool {
self.unminify_js
}
pub fn get_player_context(&self) -> WindowGLContext { pub fn get_player_context(&self) -> WindowGLContext {
self.player_context.clone() self.player_context.clone()
} }
@ -2248,13 +2251,14 @@ impl Window {
assert!(self.document.get().is_none()); assert!(self.document.get().is_none());
assert!(document.window() == self); assert!(document.window() == self);
self.document.set(Some(document)); self.document.set(Some(document));
if !self.unminify_js {
return; set_unminified_path(self.unminify_js, &self.unminified_js_dir, "unminified-js");
}
// Set a path for the document host to store unminified scripts. set_unminified_path(
let mut path = env::current_dir().unwrap(); self.unminify_css,
path.push("unminified-js"); &self.unminified_css_dir,
*self.unminified_js_dir.borrow_mut() = Some(path.into_os_string().into_string().unwrap()); "unminified-css",
);
} }
/// Commence a new URL load which will either replace this window or scroll to a fragment. /// Commence a new URL load which will either replace this window or scroll to a fragment.
@ -2521,6 +2525,10 @@ impl Window {
self.unminified_js_dir.borrow().clone() self.unminified_js_dir.borrow().clone()
} }
pub fn unminified_css_dir(&self) -> Option<String> {
self.unminified_css_dir.borrow().clone()
}
pub fn local_script_source(&self) -> &Option<String> { pub fn local_script_source(&self) -> &Option<String> {
&self.local_script_source &self.local_script_source
} }
@ -2585,6 +2593,7 @@ impl Window {
relayout_event: bool, relayout_event: bool,
prepare_for_screenshot: bool, prepare_for_screenshot: bool,
unminify_js: bool, unminify_js: bool,
unminify_css: bool,
local_script_source: Option<String>, local_script_source: Option<String>,
userscripts_path: Option<String>, userscripts_path: Option<String>,
is_headless: bool, is_headless: bool,
@ -2661,6 +2670,7 @@ impl Window {
webxr_registry, webxr_registry,
pending_layout_images: Default::default(), pending_layout_images: Default::default(),
unminified_js_dir: Default::default(), unminified_js_dir: Default::default(),
unminified_css_dir: Default::default(),
local_script_source, local_script_source,
test_worklet: Default::default(), test_worklet: Default::default(),
paint_worklet: Default::default(), paint_worklet: Default::default(),
@ -2671,6 +2681,7 @@ impl Window {
relayout_event, relayout_event,
prepare_for_screenshot, prepare_for_screenshot,
unminify_js, unminify_js,
unminify_css,
userscripts_path, userscripts_path,
replace_surrogates, replace_surrogates,
player_context, player_context,
@ -2902,3 +2913,12 @@ fn is_named_element_with_name_attribute(elem: &Element) -> bool {
fn is_named_element_with_id_attribute(elem: &Element) -> bool { fn is_named_element_with_id_attribute(elem: &Element) -> bool {
elem.is_html_element() elem.is_html_element()
} }
fn set_unminified_path(option: bool, dir_ref: &DomRefCell<Option<String>>, folder_name: &str) {
if option {
// Set a path for the document host to store unminified files.
let mut path = env::current_dir().unwrap();
path.push(folder_name);
*dir_ref.borrow_mut() = Some(path.into_os_string().into_string().unwrap());
}
}

View file

@ -94,6 +94,8 @@ mod webdriver_handlers;
#[warn(deprecated)] #[warn(deprecated)]
mod window_named_properties; mod window_named_properties;
mod unminify;
mod links; mod links;
pub use init::init; pub use init::init;

View file

@ -707,6 +707,9 @@ pub struct ScriptThread {
/// Directory with stored unminified scripts /// Directory with stored unminified scripts
local_script_source: Option<String>, local_script_source: Option<String>,
/// Unminify Css.
unminify_css: bool,
/// Where to load userscripts from, if any. An empty string will load from /// Where to load userscripts from, if any. An empty string will load from
/// the resources/user-agent-js directory, and if the option isn't passed userscripts /// the resources/user-agent-js directory, and if the option isn't passed userscripts
/// won't be loaded /// won't be loaded
@ -1343,6 +1346,7 @@ impl ScriptThread {
prepare_for_screenshot, prepare_for_screenshot,
unminify_js: opts.unminify_js, unminify_js: opts.unminify_js,
local_script_source: opts.local_script_source.clone(), local_script_source: opts.local_script_source.clone(),
unminify_css: opts.unminify_css,
userscripts_path: opts.userscripts.clone(), userscripts_path: opts.userscripts.clone(),
headless: opts.headless, headless: opts.headless,
@ -3672,6 +3676,7 @@ impl ScriptThread {
self.relayout_event, self.relayout_event,
self.prepare_for_screenshot, self.prepare_for_screenshot,
self.unminify_js, self.unminify_js,
self.unminify_css,
self.local_script_source.clone(), self.local_script_source.clone(),
self.userscripts_path.clone(), self.userscripts_path.clone(),
self.headless, self.headless,

View file

@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::io::{Read, Seek, Write};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use base::id::PipelineId; use base::id::PipelineId;
@ -42,6 +43,9 @@ use crate::dom::shadowroot::ShadowRoot;
use crate::fetch::create_a_potential_cors_request; use crate::fetch::create_a_potential_cors_request;
use crate::network_listener::{self, PreInvoke, ResourceTimingListener}; use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
use crate::unminify::{
create_output_file, create_temp_files, execute_js_beautify, BeautifyFileType,
};
pub trait StylesheetOwner { pub trait StylesheetOwner {
/// Returns whether this element was inserted by the parser (i.e., it should /// Returns whether this element was inserted by the parser (i.e., it should
@ -88,6 +92,41 @@ pub struct StylesheetContext {
resource_timing: ResourceFetchTiming, resource_timing: ResourceFetchTiming,
} }
impl StylesheetContext {
fn unminify_css(&self, data: Vec<u8>, file_url: ServoUrl) -> Vec<u8> {
if self.document.root().window().unminified_css_dir().is_none() {
return data;
}
let mut style_content = data;
if let Some((input, mut output)) = create_temp_files() {
if execute_js_beautify(
input.path(),
output.try_clone().unwrap(),
BeautifyFileType::Css,
) {
output.seek(std::io::SeekFrom::Start(0)).unwrap();
output.read_to_end(&mut style_content).unwrap();
}
}
match create_output_file(
self.document.root().window().unminified_css_dir(),
&file_url,
None,
) {
Ok(mut file) => {
file.write_all(&style_content).unwrap();
},
Err(why) => {
log::warn!("Could not store script {:?}", why);
},
}
style_content
}
}
impl PreInvoke for StylesheetContext {} impl PreInvoke for StylesheetContext {}
impl FetchResponseListener for StylesheetContext { impl FetchResponseListener for StylesheetContext {
@ -134,7 +173,8 @@ impl FetchResponseListener for StylesheetContext {
}); });
let data = if is_css { let data = if is_css {
std::mem::take(&mut self.data) let data = std::mem::take(&mut self.data);
self.unminify_css(data, metadata.final_url.clone())
} else { } else {
vec![] vec![]
}; };

View file

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fs::{create_dir_all, File};
use std::io::{Error, ErrorKind};
use std::path::{Path, PathBuf};
use std::process::Command;
use servo_url::ServoUrl;
use tempfile::NamedTempFile;
use uuid::Uuid;
pub fn create_temp_files() -> Option<(NamedTempFile, File)> {
// Write the minified code to a temporary file and pass its path as an argument
// to js-beautify to read from. Meanwhile, redirect the process' stdout into
// another temporary file and read that into a string. This avoids some hangs
// observed on macOS when using direct input/output pipes with very large
// unminified content.
let (input, output) = (NamedTempFile::new(), tempfile::tempfile());
if let (Ok(input), Ok(output)) = (input, output) {
Some((input, output))
} else {
log::warn!("Error creating input and output temp files");
None
}
}
#[derive(Debug)]
pub enum BeautifyFileType {
Css,
Js,
}
pub fn execute_js_beautify(input: &Path, output: File, file_type: BeautifyFileType) -> bool {
let mut cmd = Command::new("js-beautify");
match file_type {
BeautifyFileType::Js => (),
BeautifyFileType::Css => {
cmd.arg("--type").arg("css");
},
}
match cmd.arg(input).stdout(output).status() {
Ok(status) => status.success(),
_ => {
log::warn!(
"Failed to execute js-beautify --type {:?}, Will store unmodified script",
file_type
);
false
},
}
}
pub fn create_output_file(
unminified_dir: Option<String>,
url: &ServoUrl,
external: Option<bool>,
) -> Result<File, Error> {
let path = match unminified_dir {
Some(unminified_dir) => PathBuf::from(unminified_dir),
None => {
warn!("Unminified file directory not found");
return Err(Error::new(
ErrorKind::NotFound,
"Unminified file directory not found",
));
},
};
let (base, has_name) = match url.as_str().ends_with('/') {
true => (
path.join(&url[url::Position::BeforeHost..])
.as_path()
.to_owned(),
false,
),
false => (
path.join(&url[url::Position::BeforeHost..])
.parent()
.unwrap()
.to_owned(),
true,
),
};
create_dir_all(&base)?;
let path = if external.unwrap_or(true) && has_name {
// External.
path.join(&url[url::Position::BeforeHost..])
} else {
// Inline file or url ends with '/'
base.join(Uuid::new_v4().to_string())
};
debug!("Unminified files will be stored in {:?}", path);
File::create(path)
}