builder.rs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. use crate::{
  2. config::{CrateConfig, ExecutableType},
  3. error::{Error, Result},
  4. tools::Tool,
  5. DioxusConfig,
  6. };
  7. use cargo_metadata::{diagnostic::Diagnostic, Message};
  8. use indicatif::{ProgressBar, ProgressStyle};
  9. use serde::Serialize;
  10. use std::{
  11. fs::{copy, create_dir_all, File},
  12. io::Read,
  13. panic,
  14. path::PathBuf,
  15. time::Duration,
  16. };
  17. use wasm_bindgen_cli_support::Bindgen;
  18. #[derive(Serialize, Debug, Clone)]
  19. pub struct BuildResult {
  20. pub warnings: Vec<Diagnostic>,
  21. pub elapsed_time: u128,
  22. }
  23. #[allow(unused)]
  24. pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
  25. // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
  26. // [2] Generate the appropriate build folders
  27. // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
  28. // [4] Wasm-opt the .wasm file with whatever optimizations need to be done
  29. // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
  30. // [6] Link up the html page to the wasm module
  31. let CrateConfig {
  32. out_dir,
  33. crate_dir,
  34. target_dir,
  35. asset_dir,
  36. executable,
  37. dioxus_config,
  38. ..
  39. } = config;
  40. // start to build the assets
  41. let ignore_files = build_assets(config)?;
  42. let t_start = std::time::Instant::now();
  43. // [1] Build the .wasm module
  44. log::info!("🚅 Running build command...");
  45. let wasm_check_command = std::process::Command::new("rustup")
  46. .args(["show"])
  47. .output()?;
  48. let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
  49. if !wasm_check_output.contains("wasm32-unknown-unknown") {
  50. log::info!("wasm32-unknown-unknown target not detected, installing..");
  51. let _ = std::process::Command::new("rustup")
  52. .args(["target", "add", "wasm32-unknown-unknown"])
  53. .output()?;
  54. }
  55. let cmd = subprocess::Exec::cmd("cargo");
  56. let cmd = cmd
  57. .cwd(crate_dir)
  58. .arg("build")
  59. .arg("--target")
  60. .arg("wasm32-unknown-unknown")
  61. .arg("--message-format=json");
  62. let cmd = if config.release {
  63. cmd.arg("--release")
  64. } else {
  65. cmd
  66. };
  67. let cmd = if config.verbose {
  68. cmd.arg("--verbose")
  69. } else {
  70. cmd.arg("--quiet")
  71. };
  72. let cmd = if config.custom_profile.is_some() {
  73. let custom_profile = config.custom_profile.as_ref().unwrap();
  74. cmd.arg("--profile").arg(custom_profile)
  75. } else {
  76. cmd
  77. };
  78. let cmd = if config.features.is_some() {
  79. let features_str = config.features.as_ref().unwrap().join(" ");
  80. cmd.arg("--features").arg(features_str)
  81. } else {
  82. cmd
  83. };
  84. let cmd = cmd.args(&config.cargo_args);
  85. let cmd = match executable {
  86. ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
  87. ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
  88. ExecutableType::Example(name) => cmd.arg("--example").arg(name),
  89. };
  90. let warning_messages = prettier_build(cmd)?;
  91. // [2] Establish the output directory structure
  92. let bindgen_outdir = out_dir.join("assets").join("dioxus");
  93. let build_profile = if config.custom_profile.is_some() {
  94. config.custom_profile.as_ref().unwrap()
  95. } else if config.release {
  96. "release"
  97. } else {
  98. "debug"
  99. };
  100. let input_path = match executable {
  101. ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
  102. .join(format!("wasm32-unknown-unknown/{}", build_profile))
  103. .join(format!("{}.wasm", name)),
  104. ExecutableType::Example(name) => target_dir
  105. .join(format!("wasm32-unknown-unknown/{}/examples", build_profile))
  106. .join(format!("{}.wasm", name)),
  107. };
  108. let bindgen_result = panic::catch_unwind(move || {
  109. // [3] Bindgen the final binary for use easy linking
  110. let mut bindgen_builder = Bindgen::new();
  111. bindgen_builder
  112. .input_path(input_path)
  113. .web(true)
  114. .unwrap()
  115. .debug(true)
  116. .demangle(true)
  117. .keep_debug(true)
  118. .remove_name_section(false)
  119. .remove_producers_section(false)
  120. .out_name(&dioxus_config.application.name)
  121. .generate(&bindgen_outdir)
  122. .unwrap();
  123. });
  124. if bindgen_result.is_err() {
  125. return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
  126. }
  127. // check binaryen:wasm-opt tool
  128. let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
  129. if dioxus_tools.contains_key("binaryen") {
  130. let info = dioxus_tools.get("binaryen").unwrap();
  131. let binaryen = crate::tools::Tool::Binaryen;
  132. if binaryen.is_installed() {
  133. if let Some(sub) = info.as_table() {
  134. if sub.contains_key("wasm_opt")
  135. && sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
  136. {
  137. log::info!("Optimizing WASM size with wasm-opt...");
  138. let target_file = out_dir
  139. .join("assets")
  140. .join("dioxus")
  141. .join(format!("{}_bg.wasm", dioxus_config.application.name));
  142. if target_file.is_file() {
  143. let mut args = vec![
  144. target_file.to_str().unwrap(),
  145. "-o",
  146. target_file.to_str().unwrap(),
  147. ];
  148. if config.release {
  149. args.push("-Oz");
  150. }
  151. binaryen.call("wasm-opt", args)?;
  152. }
  153. }
  154. }
  155. } else {
  156. log::warn!(
  157. "Binaryen tool not found, you can use `dx tool add binaryen` to install it."
  158. );
  159. }
  160. }
  161. // [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
  162. if dioxus_tools.contains_key("tailwindcss") {
  163. let info = dioxus_tools.get("tailwindcss").unwrap();
  164. let tailwind = crate::tools::Tool::Tailwind;
  165. if tailwind.is_installed() {
  166. if let Some(sub) = info.as_table() {
  167. log::info!("Building Tailwind bundle CSS file...");
  168. let input_path = match sub.get("input") {
  169. Some(val) => val.as_str().unwrap(),
  170. None => "./public",
  171. };
  172. let config_path = match sub.get("config") {
  173. Some(val) => val.as_str().unwrap(),
  174. None => "./src/tailwind.config.js",
  175. };
  176. let mut args = vec![
  177. "-i",
  178. input_path,
  179. "-o",
  180. "dist/tailwind.css",
  181. "-c",
  182. config_path,
  183. ];
  184. if config.release {
  185. args.push("--minify");
  186. }
  187. tailwind.call("tailwindcss", args)?;
  188. }
  189. } else {
  190. log::warn!(
  191. "Tailwind tool not found, you can use `dx tool add tailwindcss` to install it."
  192. );
  193. }
  194. }
  195. // this code will copy all public file to the output dir
  196. let copy_options = fs_extra::dir::CopyOptions {
  197. overwrite: true,
  198. skip_exist: false,
  199. buffer_size: 64000,
  200. copy_inside: false,
  201. content_only: false,
  202. depth: 0,
  203. };
  204. if asset_dir.is_dir() {
  205. for entry in std::fs::read_dir(asset_dir)? {
  206. let path = entry?.path();
  207. if path.is_file() {
  208. std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
  209. } else {
  210. match fs_extra::dir::copy(&path, out_dir, &copy_options) {
  211. Ok(_) => {}
  212. Err(_e) => {
  213. log::warn!("Error copying dir: {}", _e);
  214. }
  215. }
  216. for ignore in &ignore_files {
  217. let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
  218. let ignore = config.out_dir.join(ignore);
  219. if ignore.is_file() {
  220. std::fs::remove_file(ignore)?;
  221. }
  222. }
  223. }
  224. }
  225. }
  226. Ok(BuildResult {
  227. warnings: warning_messages,
  228. elapsed_time: t_start.elapsed().as_millis(),
  229. })
  230. }
  231. pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
  232. log::info!("🚅 Running build [Desktop] command...");
  233. let t_start = std::time::Instant::now();
  234. let ignore_files = build_assets(config)?;
  235. let mut cmd = subprocess::Exec::cmd("cargo")
  236. .cwd(&config.crate_dir)
  237. .arg("build")
  238. .arg("--message-format=json");
  239. if config.release {
  240. cmd = cmd.arg("--release");
  241. }
  242. if config.verbose {
  243. cmd = cmd.arg("--verbose");
  244. } else {
  245. cmd = cmd.arg("--quiet");
  246. }
  247. if config.custom_profile.is_some() {
  248. let custom_profile = config.custom_profile.as_ref().unwrap();
  249. cmd = cmd.arg("--profile").arg(custom_profile);
  250. }
  251. if config.features.is_some() {
  252. let features_str = config.features.as_ref().unwrap().join(" ");
  253. cmd = cmd.arg("--features").arg(features_str);
  254. }
  255. if let Some(target) = &config.target {
  256. cmd = cmd.arg("--target").arg(target);
  257. }
  258. let target_platform = config.target.as_deref().unwrap_or("");
  259. cmd = cmd.args(&config.cargo_args);
  260. let cmd = match &config.executable {
  261. crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
  262. crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
  263. crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
  264. };
  265. let warning_messages = prettier_build(cmd)?;
  266. let release_type = match config.release {
  267. true => "release",
  268. false => "debug",
  269. };
  270. let file_name: String;
  271. let mut res_path = match &config.executable {
  272. crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
  273. file_name = name.clone();
  274. config
  275. .target_dir
  276. .join(target_platform)
  277. .join(release_type)
  278. .join(name)
  279. }
  280. crate::ExecutableType::Example(name) => {
  281. file_name = name.clone();
  282. config
  283. .target_dir
  284. .join(target_platform)
  285. .join(release_type)
  286. .join("examples")
  287. .join(name)
  288. }
  289. };
  290. let target_file = if cfg!(windows) {
  291. res_path.set_extension("exe");
  292. format!("{}.exe", &file_name)
  293. } else {
  294. file_name
  295. };
  296. if !config.out_dir.is_dir() {
  297. create_dir_all(&config.out_dir)?;
  298. }
  299. copy(res_path, config.out_dir.join(target_file))?;
  300. // this code will copy all public file to the output dir
  301. if config.asset_dir.is_dir() {
  302. let copy_options = fs_extra::dir::CopyOptions {
  303. overwrite: true,
  304. skip_exist: false,
  305. buffer_size: 64000,
  306. copy_inside: false,
  307. content_only: false,
  308. depth: 0,
  309. };
  310. for entry in std::fs::read_dir(&config.asset_dir)? {
  311. let path = entry?.path();
  312. if path.is_file() {
  313. std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
  314. } else {
  315. match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
  316. Ok(_) => {}
  317. Err(e) => {
  318. log::warn!("Error copying dir: {}", e);
  319. }
  320. }
  321. for ignore in &ignore_files {
  322. let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
  323. let ignore = config.out_dir.join(ignore);
  324. if ignore.is_file() {
  325. std::fs::remove_file(ignore)?;
  326. }
  327. }
  328. }
  329. }
  330. }
  331. log::info!(
  332. "🚩 Build completed: [./{}]",
  333. config
  334. .dioxus_config
  335. .application
  336. .out_dir
  337. .clone()
  338. .unwrap_or_else(|| PathBuf::from("dist"))
  339. .display()
  340. );
  341. println!("build desktop done");
  342. Ok(BuildResult {
  343. warnings: warning_messages,
  344. elapsed_time: t_start.elapsed().as_millis(),
  345. })
  346. }
  347. fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
  348. let mut warning_messages: Vec<Diagnostic> = vec![];
  349. let pb = ProgressBar::new_spinner();
  350. pb.enable_steady_tick(Duration::from_millis(200));
  351. pb.set_style(
  352. ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
  353. .unwrap()
  354. .tick_chars("/|\\- "),
  355. );
  356. pb.set_message("💼 Waiting to start build the project...");
  357. struct StopSpinOnDrop(ProgressBar);
  358. impl Drop for StopSpinOnDrop {
  359. fn drop(&mut self) {
  360. self.0.finish_and_clear();
  361. }
  362. }
  363. let stdout = cmd.detached().stream_stdout()?;
  364. let reader = std::io::BufReader::new(stdout);
  365. for message in cargo_metadata::Message::parse_stream(reader) {
  366. match message.unwrap() {
  367. Message::CompilerMessage(msg) => {
  368. let message = msg.message;
  369. match message.level {
  370. cargo_metadata::diagnostic::DiagnosticLevel::Error => {
  371. return {
  372. Err(anyhow::anyhow!(message
  373. .rendered
  374. .unwrap_or("Unknown".into())))
  375. };
  376. }
  377. cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
  378. warning_messages.push(message.clone());
  379. }
  380. _ => {}
  381. }
  382. }
  383. Message::CompilerArtifact(artifact) => {
  384. pb.set_message(format!("⚙️ Compiling {} ", artifact.package_id));
  385. pb.tick();
  386. }
  387. Message::BuildScriptExecuted(script) => {
  388. let _package_id = script.package_id.to_string();
  389. }
  390. Message::BuildFinished(finished) => {
  391. if finished.success {
  392. log::info!("👑 Build done.");
  393. } else {
  394. std::process::exit(1);
  395. }
  396. }
  397. _ => (), // Unknown message
  398. }
  399. }
  400. Ok(warning_messages)
  401. }
  402. pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
  403. let crate_root = crate::cargo::crate_root().unwrap();
  404. let custom_html_file = crate_root.join("index.html");
  405. let mut html = if custom_html_file.is_file() {
  406. let mut buf = String::new();
  407. let mut file = File::open(custom_html_file).unwrap();
  408. if file.read_to_string(&mut buf).is_ok() {
  409. buf
  410. } else {
  411. String::from(include_str!("./assets/index.html"))
  412. }
  413. } else {
  414. String::from(include_str!("./assets/index.html"))
  415. };
  416. let resources = config.web.resource.clone();
  417. let mut style_list = resources.style.unwrap_or_default();
  418. let mut script_list = resources.script.unwrap_or_default();
  419. if serve {
  420. let mut dev_style = resources.dev.style.clone().unwrap_or_default();
  421. let mut dev_script = resources.dev.script.unwrap_or_default();
  422. style_list.append(&mut dev_style);
  423. script_list.append(&mut dev_script);
  424. }
  425. let mut style_str = String::new();
  426. for style in style_list {
  427. style_str.push_str(&format!(
  428. "<link rel=\"stylesheet\" href=\"{}\">\n",
  429. &style.to_str().unwrap(),
  430. ))
  431. }
  432. if config
  433. .application
  434. .tools
  435. .clone()
  436. .unwrap_or_default()
  437. .contains_key("tailwindcss")
  438. {
  439. style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
  440. }
  441. replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
  442. let mut script_str = String::new();
  443. for script in script_list {
  444. script_str.push_str(&format!(
  445. "<script src=\"{}\"></script>\n",
  446. &script.to_str().unwrap(),
  447. ))
  448. }
  449. replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
  450. if serve {
  451. html += &format!(
  452. "<script>{}</script>",
  453. include_str!("./assets/autoreload.js")
  454. );
  455. }
  456. let base_path = match &config.web.app.base_path {
  457. Some(path) => path,
  458. None => ".",
  459. };
  460. let app_name = &config.application.name;
  461. // Check if a script already exists
  462. if html.contains("{app_name}") && html.contains("{base_path}") {
  463. html = html.replace("{app_name}", app_name);
  464. html = html.replace("{base_path}", base_path);
  465. } else {
  466. // If not, insert the script
  467. html = html.replace(
  468. "</body",
  469. &format!(
  470. r#"<script type="module">
  471. import init from "/{base_path}/assets/dioxus/{app_name}.js";
  472. init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {{
  473. if (wasm.__wbindgen_start == undefined) {{
  474. wasm.main();
  475. }}
  476. }});
  477. </script>
  478. </body"#
  479. ),
  480. );
  481. }
  482. let title = config
  483. .web
  484. .app
  485. .title
  486. .clone()
  487. .unwrap_or_else(|| "dioxus | ⛺".into());
  488. replace_or_insert_before("{app_title}", &title, "</title", &mut html);
  489. html
  490. }
  491. fn replace_or_insert_before(
  492. replace: &str,
  493. with: &str,
  494. or_insert_before: &str,
  495. content: &mut String,
  496. ) {
  497. if content.contains(replace) {
  498. *content = content.replace(replace, with);
  499. } else {
  500. *content = content.replace(or_insert_before, &format!("{}{}", with, or_insert_before));
  501. }
  502. }
  503. // this function will build some assets file
  504. // like sass tool resources
  505. // this function will return a array which file don't need copy to out_dir.
  506. fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
  507. let mut result = vec![];
  508. let dioxus_config = &config.dioxus_config;
  509. let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
  510. // check sass tool state
  511. let sass = Tool::Sass;
  512. if sass.is_installed() && dioxus_tools.contains_key("sass") {
  513. let sass_conf = dioxus_tools.get("sass").unwrap();
  514. if let Some(tab) = sass_conf.as_table() {
  515. let source_map = tab.contains_key("source_map");
  516. let source_map = if source_map && tab.get("source_map").unwrap().is_bool() {
  517. if tab.get("source_map").unwrap().as_bool().unwrap_or_default() {
  518. "--source-map"
  519. } else {
  520. "--no-source-map"
  521. }
  522. } else {
  523. "--source-map"
  524. };
  525. if tab.contains_key("input") {
  526. if tab.get("input").unwrap().is_str() {
  527. let file = tab.get("input").unwrap().as_str().unwrap().trim();
  528. if file == "*" {
  529. // if the sass open auto, we need auto-check the assets dir.
  530. let asset_dir = config.asset_dir.clone();
  531. if asset_dir.is_dir() {
  532. for entry in walkdir::WalkDir::new(&asset_dir)
  533. .into_iter()
  534. .filter_map(|e| e.ok())
  535. {
  536. let temp = entry.path();
  537. if temp.is_file() {
  538. let suffix = temp.extension();
  539. if suffix.is_none() {
  540. continue;
  541. }
  542. let suffix = suffix.unwrap().to_str().unwrap();
  543. if suffix == "scss" || suffix == "sass" {
  544. // if file suffix is `scss` / `sass` we need transform it.
  545. let out_file = format!(
  546. "{}.css",
  547. temp.file_stem().unwrap().to_str().unwrap()
  548. );
  549. let target_path = config
  550. .out_dir
  551. .join(
  552. temp.strip_prefix(&asset_dir)
  553. .unwrap()
  554. .parent()
  555. .unwrap(),
  556. )
  557. .join(out_file);
  558. let res = sass.call(
  559. "sass",
  560. vec![
  561. temp.to_str().unwrap(),
  562. target_path.to_str().unwrap(),
  563. source_map,
  564. ],
  565. );
  566. if res.is_ok() {
  567. result.push(temp.to_path_buf());
  568. }
  569. }
  570. }
  571. }
  572. }
  573. } else {
  574. // just transform one file.
  575. let relative_path = if &file[0..1] == "/" {
  576. &file[1..file.len()]
  577. } else {
  578. file
  579. };
  580. let path = config.asset_dir.join(relative_path);
  581. let out_file =
  582. format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
  583. let target_path = config
  584. .out_dir
  585. .join(PathBuf::from(relative_path).parent().unwrap())
  586. .join(out_file);
  587. if path.is_file() {
  588. let res = sass.call(
  589. "sass",
  590. vec![
  591. path.to_str().unwrap(),
  592. target_path.to_str().unwrap(),
  593. source_map,
  594. ],
  595. );
  596. if res.is_ok() {
  597. result.push(path);
  598. } else {
  599. log::error!("{:?}", res);
  600. }
  601. }
  602. }
  603. } else if tab.get("input").unwrap().is_array() {
  604. // check files list.
  605. let list = tab.get("input").unwrap().as_array().unwrap();
  606. for i in list {
  607. if i.is_str() {
  608. let path = i.as_str().unwrap();
  609. let relative_path = if &path[0..1] == "/" {
  610. &path[1..path.len()]
  611. } else {
  612. path
  613. };
  614. let path = config.asset_dir.join(relative_path);
  615. let out_file =
  616. format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
  617. let target_path = config
  618. .out_dir
  619. .join(PathBuf::from(relative_path).parent().unwrap())
  620. .join(out_file);
  621. if path.is_file() {
  622. let res = sass.call(
  623. "sass",
  624. vec![
  625. path.to_str().unwrap(),
  626. target_path.to_str().unwrap(),
  627. source_map,
  628. ],
  629. );
  630. if res.is_ok() {
  631. result.push(path);
  632. }
  633. }
  634. }
  635. }
  636. }
  637. }
  638. }
  639. }
  640. // SASS END
  641. Ok(result)
  642. }