hotreload_pattern.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. #![allow(unused)]
  2. use dioxus_core::{prelude::Template, VNode};
  3. use dioxus_rsx::{
  4. hot_reload::{diff_rsx, ChangedRsx},
  5. hotreload::HotReloadedTemplate,
  6. CallBody, HotReloadingContext,
  7. };
  8. use proc_macro2::TokenStream;
  9. use quote::{quote, ToTokens};
  10. use syn::{parse::Parse, spanned::Spanned, token::Token, File};
  11. #[derive(Debug)]
  12. struct Mock;
  13. impl HotReloadingContext for Mock {
  14. fn map_attribute(
  15. element_name_rust: &str,
  16. attribute_name_rust: &str,
  17. ) -> Option<(&'static str, Option<&'static str>)> {
  18. match element_name_rust {
  19. "svg" => match attribute_name_rust {
  20. "width" => Some(("width", Some("style"))),
  21. "height" => Some(("height", Some("style"))),
  22. _ => None,
  23. },
  24. _ => None,
  25. }
  26. }
  27. fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
  28. match element_name_rust {
  29. "svg" => Some(("svg", Some("svg"))),
  30. _ => None,
  31. }
  32. }
  33. }
  34. fn boilerplate(old: TokenStream, new: TokenStream) -> Option<Vec<Template>> {
  35. let old: CallBody = syn::parse2(old).unwrap();
  36. let new: CallBody = syn::parse2(new).unwrap();
  37. let location = "file:line:col:0";
  38. hotreload_callbody::<Mock>(&old, &new, location)
  39. }
  40. fn can_hotreload(old: TokenStream, new: TokenStream) -> Option<HotReloadedTemplate> {
  41. let old: CallBody = syn::parse2(old).unwrap();
  42. let new: CallBody = syn::parse2(new).unwrap();
  43. let location = "file:line:col:0";
  44. let results = HotReloadedTemplate::new::<Mock>(&old, &new, location, Default::default())?;
  45. Some(results)
  46. }
  47. fn hotreload_callbody<Ctx: HotReloadingContext>(
  48. old: &CallBody,
  49. new: &CallBody,
  50. location: &'static str,
  51. ) -> Option<Vec<Template>> {
  52. let results = HotReloadedTemplate::new::<Ctx>(old, new, location, Default::default())?;
  53. Some(results.templates)
  54. }
  55. fn callbody_to_template<Ctx: HotReloadingContext>(
  56. old: &CallBody,
  57. location: &'static str,
  58. ) -> Option<Template> {
  59. let results = HotReloadedTemplate::new::<Ctx>(old, old, location, Default::default())?;
  60. Some(*results.templates.first().unwrap())
  61. }
  62. fn base_stream() -> TokenStream {
  63. quote! {
  64. div {
  65. for item in vec![1, 2, 3] {
  66. div { "asasddasdasd" }
  67. }
  68. for item in vec![4, 5, 6] {
  69. div { "asasddasdasd" }
  70. }
  71. }
  72. }
  73. }
  74. fn base() -> CallBody {
  75. syn::parse2(base_stream()).unwrap()
  76. }
  77. #[test]
  78. fn simple_for_loop() {
  79. let old = quote! {
  80. div {
  81. for item in vec![1, 2, 3] {
  82. div { "asasddasdasd" }
  83. }
  84. }
  85. };
  86. let new_valid = quote! {
  87. div {
  88. for item in vec![1, 2, 3] {
  89. div { "asasddasdasd" }
  90. div { "123" }
  91. }
  92. }
  93. };
  94. let new_invalid = quote! {
  95. div {
  96. for item in vec![1, 2, 3, 4] {
  97. div { "asasddasdasd" }
  98. div { "123" }
  99. }
  100. }
  101. };
  102. let location = "file:line:col:0";
  103. let old: CallBody = syn::parse2(old).unwrap();
  104. let new_valid: CallBody = syn::parse2(new_valid).unwrap();
  105. let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
  106. assert!(hotreload_callbody::<Mock>(&old, &new_valid, location).is_some());
  107. assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
  108. }
  109. #[test]
  110. fn valid_reorder() {
  111. let old = base();
  112. let new_valid = quote! {
  113. div {
  114. for item in vec![4, 5, 6] {
  115. span { "asasddasdasd" }
  116. span { "123" }
  117. }
  118. for item in vec![1, 2, 3] {
  119. div { "asasddasdasd" }
  120. div { "123" }
  121. }
  122. }
  123. };
  124. let location = "file:line:col:0";
  125. let new: CallBody = syn::parse2(new_valid).unwrap();
  126. let valid = hotreload_callbody::<Mock>(&old, &new, location);
  127. assert!(valid.is_some());
  128. let templates = valid.unwrap();
  129. // Currently we return all the templates, even if they didn't change
  130. assert_eq!(templates.len(), 3);
  131. let template = &templates[2];
  132. // It's an inversion, so we should get them in reverse
  133. assert_eq!(template.node_paths, &[&[0, 1], &[0, 0]]);
  134. // And the byte index should be the original template
  135. assert_eq!(template.name, "file:line:col:0");
  136. }
  137. #[test]
  138. fn invalid_cases() {
  139. let new_invalid = quote! {
  140. div {
  141. for item in vec![1, 2, 3, 4] {
  142. div { "asasddasdasd" }
  143. div { "123" }
  144. }
  145. for item in vec![4, 5, 6] {
  146. span { "asasddasdasd" }
  147. span { "123" }
  148. }
  149. }
  150. };
  151. // just remove an entire for loop
  152. let new_valid_removed = quote! {
  153. div {
  154. for item in vec![4, 5, 6] {
  155. span { "asasddasdasd" }
  156. span { "123" }
  157. }
  158. }
  159. };
  160. let new_invalid_new_dynamic_internal = quote! {
  161. div {
  162. for item in vec![1, 2, 3] {
  163. div { "asasddasdasd" }
  164. div { "123" }
  165. }
  166. for item in vec![4, 5, 6] {
  167. span { "asasddasdasd" }
  168. // this is a new dynamic node, and thus can't be hot reloaded
  169. // Eventually we might be able to do a format like this, but not right now
  170. span { "123 {item}" }
  171. }
  172. }
  173. };
  174. let new_invalid_added = quote! {
  175. div {
  176. for item in vec![1, 2, 3] {
  177. div { "asasddasdasd" }
  178. div { "123" }
  179. }
  180. for item in vec![4, 5, 6] {
  181. span { "asasddasdasd" }
  182. span { "123" }
  183. }
  184. for item in vec![7, 8, 9] {
  185. span { "asasddasdasd" }
  186. span { "123" }
  187. }
  188. }
  189. };
  190. let location = "file:line:col:0";
  191. let old = base();
  192. let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
  193. let new_valid_removed: CallBody = syn::parse2(new_valid_removed).unwrap();
  194. let new_invalid_new_dynamic_internal: CallBody =
  195. syn::parse2(new_invalid_new_dynamic_internal).unwrap();
  196. let new_invalid_added: CallBody = syn::parse2(new_invalid_added).unwrap();
  197. assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
  198. assert!(
  199. hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal, location).is_none()
  200. );
  201. let removed = hotreload_callbody::<Mock>(&old, &new_valid_removed, location);
  202. assert!(removed.is_some());
  203. let templates = removed.unwrap();
  204. // we don't get the removed template back
  205. assert_eq!(templates.len(), 2);
  206. let template = &templates[1];
  207. // We just completely removed the dynamic node, so it should be a "dud" path and then the placement
  208. assert_eq!(template.node_paths, &[&[], &[0u8, 0] as &[u8]]);
  209. // Adding a new dynamic node should not be hot reloadable
  210. let added = hotreload_callbody::<Mock>(&old, &new_invalid_added, location);
  211. assert!(added.is_none());
  212. }
  213. #[test]
  214. fn new_names() {
  215. let old = quote! {
  216. div {
  217. for item in vec![1, 2, 3] {
  218. div { "asasddasdasd" }
  219. div { "123" }
  220. }
  221. }
  222. };
  223. // Same order, just different contents
  224. let new_valid_internal = quote! {
  225. div {
  226. for item in vec![1, 2, 3] {
  227. div { "asasddasdasd" }
  228. div { "456" }
  229. }
  230. }
  231. };
  232. let templates = boilerplate(old, new_valid_internal).unwrap();
  233. // Getting back all the templates even though some might not have changed
  234. // This is currently just a symptom of us not checking if anything has changed, but has no bearing
  235. // on output really.
  236. assert_eq!(templates.len(), 2);
  237. // The ordering is going to be inverse since its a depth-first traversal
  238. let external = &templates[1];
  239. assert_eq!(external.name, "file:line:col:0");
  240. let internal = &templates[0];
  241. assert_eq!(internal.name, "file:line:col:1");
  242. }
  243. #[test]
  244. fn attributes_reload() {
  245. let old = quote! {
  246. div {
  247. class: "{class}",
  248. id: "{id}",
  249. name: "name",
  250. }
  251. };
  252. // Same order, just different contents
  253. let new_valid_internal = quote! {
  254. div {
  255. id: "{id}",
  256. name: "name",
  257. class: "{class}"
  258. }
  259. };
  260. let templates = boilerplate(old, new_valid_internal).unwrap();
  261. dbg!(templates);
  262. }
  263. #[test]
  264. fn template_generates() {
  265. let old = quote! {
  266. svg {
  267. width: 100,
  268. height: "100px",
  269. "width2": 100,
  270. "height2": "100px",
  271. p { "hello world" }
  272. {(0..10).map(|i| rsx!{"{i}"})}
  273. }
  274. div {
  275. width: 120,
  276. div {
  277. height: "100px",
  278. "width2": 130,
  279. "height2": "100px",
  280. for i in 0..10 {
  281. div {
  282. "asdasd"
  283. }
  284. }
  285. }
  286. }
  287. };
  288. let old: CallBody = syn::parse2(old).unwrap();
  289. let template = callbody_to_template::<Mock>(&old, "file:line:col:0");
  290. }
  291. #[test]
  292. fn diffs_complex() {
  293. #[allow(unused, non_snake_case)]
  294. fn Comp() -> dioxus_core::Element {
  295. VNode::empty()
  296. }
  297. let old = quote! {
  298. svg {
  299. width: 100,
  300. height: "100px",
  301. "width2": 100,
  302. "height2": "100px",
  303. p { "hello world" }
  304. {(0..10).map(|i| rsx!{"{i}"})},
  305. {(0..10).map(|i| rsx!{"{i}"})},
  306. {(0..11).map(|i| rsx!{"{i}"})},
  307. Comp {}
  308. }
  309. };
  310. // scrambling the attributes should not cause a full rebuild
  311. let new = quote! {
  312. div {
  313. width: 100,
  314. height: "100px",
  315. "width2": 100,
  316. "height2": "100px",
  317. p { "hello world" }
  318. Comp {}
  319. {(0..10).map(|i| rsx!{"{i}"})},
  320. {(0..10).map(|i| rsx!{"{i}"})},
  321. {(0..11).map(|i| rsx!{"{i}"})},
  322. }
  323. };
  324. let old: CallBody = syn::parse2(old).unwrap();
  325. let new: CallBody = syn::parse2(new).unwrap();
  326. let location = "file:line:col:0";
  327. let templates = hotreload_callbody::<Mock>(&old, &new, location).unwrap();
  328. }
  329. #[test]
  330. fn remove_node() {
  331. let changed = boilerplate(
  332. quote! {
  333. svg {
  334. Comp {}
  335. {(0..10).map(|i| rsx!{"{i}"})},
  336. }
  337. },
  338. quote! {
  339. div {
  340. {(0..10).map(|i| rsx!{"{i}"})},
  341. }
  342. },
  343. )
  344. .unwrap();
  345. dbg!(changed);
  346. }
  347. #[test]
  348. fn if_chains() {
  349. let changed = boilerplate(
  350. quote! {
  351. if cond {
  352. "foo"
  353. }
  354. },
  355. quote! {
  356. if cond {
  357. "baz"
  358. }
  359. },
  360. )
  361. .unwrap();
  362. let very_complex_chain = boilerplate(
  363. quote! {
  364. if cond {
  365. if second_cond {
  366. "foo"
  367. }
  368. } else if othercond {
  369. "bar"
  370. } else {
  371. "baz"
  372. }
  373. },
  374. quote! {
  375. if cond {
  376. if second_cond {
  377. span { "asasddasdasd 789" }
  378. }
  379. } else if othercond {
  380. span { "asasddasdasd 123" }
  381. } else {
  382. span { "asasddasdas 456" }
  383. }
  384. },
  385. )
  386. .unwrap();
  387. dbg!(very_complex_chain);
  388. }
  389. #[test]
  390. fn component_bodies() {
  391. let changed = boilerplate(
  392. quote! {
  393. Comp {
  394. "foo"
  395. }
  396. },
  397. quote! {
  398. Comp {
  399. "baz"
  400. }
  401. },
  402. )
  403. .unwrap();
  404. dbg!(changed);
  405. }
  406. /// Everything reloads!
  407. #[test]
  408. fn kitch_sink_of_reloadability() {
  409. let changed = boilerplate(
  410. quote! {
  411. div {
  412. for i in 0..10 {
  413. div { "123" }
  414. Comp {
  415. "foo"
  416. }
  417. if cond {
  418. "foo"
  419. }
  420. }
  421. }
  422. },
  423. quote! {
  424. div {
  425. "hi!"
  426. for i in 0..10 {
  427. div { "456" }
  428. Comp { "bar" }
  429. if cond {
  430. "baz"
  431. }
  432. }
  433. }
  434. },
  435. )
  436. .unwrap();
  437. dbg!(changed);
  438. }
  439. /// Moving nodes inbetween multiple rsx! calls currently doesn't work
  440. /// Sad. Needs changes to core to work, and is technically flawed?
  441. #[test]
  442. fn entire_kitchen_sink() {
  443. let changed = boilerplate(
  444. quote! {
  445. div {
  446. for i in 0..10 {
  447. div { "123" }
  448. }
  449. Comp {
  450. "foo"
  451. }
  452. if cond {
  453. "foo"
  454. }
  455. }
  456. },
  457. quote! {
  458. div {
  459. "hi!"
  460. Comp {
  461. for i in 0..10 {
  462. div { "456" }
  463. }
  464. "bar"
  465. if cond {
  466. "baz"
  467. }
  468. }
  469. }
  470. },
  471. );
  472. assert!(changed.is_none());
  473. }
  474. #[test]
  475. fn tokenstreams_and_locations() {
  476. let changed = boilerplate(
  477. quote! {
  478. div { "hhi" }
  479. div {
  480. {rsx! { "hi again!" }},
  481. for i in 0..2 {
  482. "first"
  483. div { "hi {i}" }
  484. }
  485. for i in 0..3 {
  486. "Second"
  487. div { "hi {i}" }
  488. }
  489. if false {
  490. div { "hi again!?" }
  491. } else if true {
  492. div { "its cool?" }
  493. } else {
  494. div { "not nice !" }
  495. }
  496. }
  497. },
  498. quote! {
  499. div { "hhi" }
  500. div {
  501. {rsx! { "hi again!" }},
  502. for i in 0..2 {
  503. "first"
  504. div { "hi {i}" }
  505. }
  506. for i in 0..3 {
  507. "Second"
  508. div { "hi {i}" }
  509. }
  510. if false {
  511. div { "hi again?" }
  512. } else if true {
  513. div { "cool?" }
  514. } else {
  515. div { "nice !" }
  516. }
  517. }
  518. },
  519. );
  520. dbg!(changed);
  521. }
  522. #[test]
  523. fn ide_testcase() {
  524. let changed = boilerplate(
  525. quote! {
  526. div {
  527. div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
  528. for x in 0..5 {
  529. h3 { "For loop contents" }
  530. }
  531. }
  532. },
  533. quote! {
  534. div {
  535. div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
  536. for x in 0..5 {
  537. h3 { "For loop contents" }
  538. }
  539. }
  540. },
  541. );
  542. dbg!(changed);
  543. }
  544. #[test]
  545. fn assigns_ids() {
  546. let toks = quote! {
  547. div {
  548. div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
  549. for x in 0..5 {
  550. h3 { "For loop contents" }
  551. }
  552. }
  553. };
  554. let parsed = syn::parse2::<CallBody>(toks).unwrap();
  555. let node = parsed.body.get_dyn_node(&[0, 1]);
  556. dbg!(node);
  557. }
  558. #[test]
  559. fn simple_start() {
  560. let changed = boilerplate(
  561. //
  562. quote! {
  563. div {
  564. class: "Some {one}",
  565. id: "Something {two}",
  566. "One"
  567. }
  568. },
  569. quote! {
  570. div {
  571. id: "Something {two}",
  572. class: "Some {one}",
  573. "One"
  574. }
  575. },
  576. );
  577. dbg!(changed.unwrap());
  578. }
  579. #[test]
  580. fn complex_cases() {
  581. let changed = can_hotreload(
  582. quote! {
  583. div {
  584. class: "Some {one}",
  585. id: "Something {two}",
  586. "One"
  587. }
  588. },
  589. quote! {
  590. div {
  591. class: "Some {one}",
  592. id: "Something else {two}",
  593. "One"
  594. }
  595. },
  596. );
  597. dbg!(changed.unwrap());
  598. }
  599. #[test]
  600. fn attribute_cases() {
  601. let changed = can_hotreload(
  602. quote! {
  603. div {
  604. class: "Some {one}",
  605. id: "Something {two}",
  606. "One"
  607. }
  608. },
  609. quote! {
  610. div {
  611. id: "Something {two}",
  612. "One"
  613. }
  614. },
  615. );
  616. dbg!(changed.unwrap());
  617. let changed = can_hotreload(
  618. //
  619. quote! { div { class: 123 } },
  620. quote! { div { class: 456 } },
  621. );
  622. dbg!(changed.unwrap());
  623. let changed = can_hotreload(
  624. //
  625. quote! { div { class: 123.0 } },
  626. quote! { div { class: 456.0 } },
  627. );
  628. dbg!(changed.unwrap());
  629. let changed = can_hotreload(
  630. //
  631. quote! { div { class: "asd {123}", } },
  632. quote! { div { class: "def", } },
  633. );
  634. dbg!(changed.unwrap());
  635. }
  636. #[test]
  637. fn text_node_cases() {
  638. let changed = can_hotreload(
  639. //
  640. quote! { div { "hello {world}" } },
  641. quote! { div { "world {world}" } },
  642. );
  643. dbg!(changed.unwrap());
  644. let changed = can_hotreload(
  645. //
  646. quote! { div { "hello {world}" } },
  647. quote! { div { "world" } },
  648. );
  649. dbg!(changed.unwrap());
  650. let changed = can_hotreload(
  651. //
  652. quote! { div { "hello" } },
  653. quote! { div { "world {world}" } },
  654. );
  655. assert!(changed.is_none());
  656. }
  657. #[test]
  658. fn simple_carry() {
  659. let a = quote! {
  660. // start with
  661. "thing {abc} {def}" // 1, 1, 1
  662. "thing {def}" // 1, 0, 1
  663. "other {hij}" // 1, 1, 1
  664. };
  665. let b = quote! {
  666. // end with
  667. "thing {def}"
  668. "thing {abc}"
  669. "thing {hij}"
  670. };
  671. let changed = can_hotreload(a, b);
  672. dbg!(changed.unwrap());
  673. }
  674. #[test]
  675. fn complex_carry_text() {
  676. let a = quote! {
  677. // start with
  678. "thing {abc} {def}" // 1, 1, 1
  679. "thing {abc}" // 1, 0, 1
  680. "other {abc} {def} {hij}" // 1, 1, 1
  681. };
  682. let b = quote! {
  683. // end with
  684. "thing {abc}"
  685. "thing {hij}"
  686. };
  687. let changed = can_hotreload(a, b);
  688. dbg!(changed.unwrap());
  689. }
  690. #[test]
  691. fn complex_carry() {
  692. let a = quote! {
  693. Component {
  694. class: "thing {abc}",
  695. other: "other {abc} {def}",
  696. }
  697. Component {
  698. class: "thing {abc}",
  699. other: "other",
  700. }
  701. };
  702. let b = quote! {
  703. // how about shuffling components, for, if, etc
  704. Component {
  705. class: "thing {abc}",
  706. other: "other {abc} {def}",
  707. }
  708. Component {
  709. class: "thing",
  710. other: "other",
  711. }
  712. };
  713. let changed = can_hotreload(a, b);
  714. dbg!(changed.unwrap());
  715. }
  716. #[test]
  717. fn component_with_lits() {
  718. let a = quote! {
  719. Component {
  720. class: 123,
  721. id: 456.789,
  722. other: true,
  723. blah: "hello {world}",
  724. }
  725. };
  726. // changing lit values
  727. let b = quote! {
  728. Component {
  729. class: 456,
  730. id: 789.456,
  731. other: false,
  732. blah: "goodbye {world}",
  733. }
  734. };
  735. let changed = can_hotreload(a, b);
  736. dbg!(changed.unwrap());
  737. }
  738. #[test]
  739. fn component_with_handlers() {
  740. let a = quote! {
  741. Component {
  742. class: 123,
  743. id: 456.789,
  744. other: true,
  745. blah: "hello {world}",
  746. onclick: |e| { println!("clicked") },
  747. }
  748. };
  749. // changing lit values
  750. let b = quote! {
  751. Component {
  752. class: 456,
  753. id: 789.456,
  754. other: false,
  755. blah: "goodbye {world}",
  756. onclick: |e| { println!("clicked") },
  757. }
  758. };
  759. let changed = can_hotreload(a, b);
  760. dbg!(changed.unwrap());
  761. }