frontend.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use crate::{
  5. helpers::{app_paths::walk_builder, npm::PackageManager},
  6. Result,
  7. };
  8. use anyhow::Context;
  9. use itertools::Itertools;
  10. use magic_string::MagicString;
  11. use oxc_allocator::Allocator;
  12. use oxc_ast::ast::*;
  13. use oxc_parser::Parser;
  14. use oxc_span::SourceType;
  15. use std::{fs, path::Path};
  16. mod partial_loader;
  17. const RENAMED_MODULES: phf::Map<&str, &str> = phf::phf_map! {
  18. "tauri" => "core",
  19. "window" => "webviewWindow"
  20. };
  21. const PLUGINIFIED_MODULES: [&str; 11] = [
  22. "cli",
  23. "clipboard",
  24. "dialog",
  25. "fs",
  26. "globalShortcut",
  27. "http",
  28. "notification",
  29. "os",
  30. "process",
  31. "shell",
  32. "updater",
  33. ];
  34. // (from, to)
  35. const MODULES_MAP: phf::Map<&str, &str> = phf::phf_map! {
  36. // renamed
  37. "@tauri-apps/api/tauri" => "@tauri-apps/api/core",
  38. "@tauri-apps/api/window" => "@tauri-apps/api/webviewWindow",
  39. // pluginified
  40. "@tauri-apps/api/cli" => "@tauri-apps/plugin-cli",
  41. "@tauri-apps/api/clipboard" => "@tauri-apps/plugin-clipboard-manager",
  42. "@tauri-apps/api/dialog" => "@tauri-apps/plugin-dialog",
  43. "@tauri-apps/api/fs" => "@tauri-apps/plugin-fs",
  44. "@tauri-apps/api/globalShortcut" => "@tauri-apps/plugin-global-shortcut",
  45. "@tauri-apps/api/http" => "@tauri-apps/plugin-http",
  46. "@tauri-apps/api/notification" => "@tauri-apps/plugin-notification",
  47. "@tauri-apps/api/os" => "@tauri-apps/plugin-os",
  48. "@tauri-apps/api/process" => "@tauri-apps/plugin-process",
  49. "@tauri-apps/api/shell" => "@tauri-apps/plugin-shell",
  50. "@tauri-apps/api/updater" => "@tauri-apps/plugin-updater",
  51. // v1 plugins to v2
  52. "tauri-plugin-sql-api" => "@tauri-apps/plugin-sql",
  53. "tauri-plugin-store-api" => "@tauri-apps/plugin-store",
  54. "tauri-plugin-upload-api" => "@tauri-apps/plugin-upload",
  55. "tauri-plugin-fs-extra-api" => "@tauri-apps/plugin-fs",
  56. "tauri-plugin-fs-watch-api" => "@tauri-apps/plugin-fs",
  57. "tauri-plugin-autostart-api" => "@tauri-apps/plugin-autostart",
  58. "tauri-plugin-websocket-api" => "@tauri-apps/plugin-websocket",
  59. "tauri-plugin-positioner-api" => "@tauri-apps/plugin-positioner",
  60. "tauri-plugin-stronghold-api" => "@tauri-apps/plugin-stronghold",
  61. "tauri-plugin-window-state-api" => "@tauri-apps/plugin-window-state",
  62. "tauri-plugin-authenticator-api" => "@tauri-apps/plugin-authenticator",
  63. };
  64. const JS_EXTENSIONS: &[&str] = &["js", "mjs", "jsx", "ts", "mts", "tsx", "svelte", "vue"];
  65. /// Returns a list of migrated plugins
  66. pub fn migrate(app_dir: &Path) -> Result<Vec<String>> {
  67. let mut new_npm_packages = Vec::new();
  68. let mut new_plugins = Vec::new();
  69. let mut npm_packages_to_remove = Vec::new();
  70. let pre = env!("CARGO_PKG_VERSION_PRE");
  71. let npm_version = if pre.is_empty() {
  72. format!("{}.0.0", env!("CARGO_PKG_VERSION_MAJOR"))
  73. } else {
  74. format!(
  75. "{}.0.0-{}.0",
  76. env!("CARGO_PKG_VERSION_MAJOR"),
  77. pre.split('.').next().unwrap()
  78. )
  79. };
  80. let pm = PackageManager::from_project(app_dir)
  81. .into_iter()
  82. .next()
  83. .unwrap_or(PackageManager::Npm);
  84. for pkg in ["@tauri-apps/cli", "@tauri-apps/api"] {
  85. let version = pm
  86. .current_package_version(pkg, app_dir)
  87. .unwrap_or_default()
  88. .unwrap_or_default();
  89. if version.starts_with('1') {
  90. new_npm_packages.push(format!("{pkg}@^{npm_version}"));
  91. }
  92. }
  93. for entry in walk_builder(app_dir).build().flatten() {
  94. if entry.file_type().map(|t| t.is_file()).unwrap_or_default() {
  95. let path = entry.path();
  96. let ext = path.extension().unwrap_or_default();
  97. if JS_EXTENSIONS.iter().any(|e| e == &ext) {
  98. let js_contents = std::fs::read_to_string(path)?;
  99. let new_contents = migrate_imports(
  100. path,
  101. &js_contents,
  102. &mut new_plugins,
  103. &mut npm_packages_to_remove,
  104. )?;
  105. if new_contents != js_contents {
  106. fs::write(path, new_contents)
  107. .with_context(|| format!("Error writing {}", path.display()))?;
  108. }
  109. }
  110. }
  111. }
  112. if !npm_packages_to_remove.is_empty() {
  113. npm_packages_to_remove.sort();
  114. npm_packages_to_remove.dedup();
  115. pm.remove(&npm_packages_to_remove, app_dir)
  116. .context("Error removing npm packages")?;
  117. }
  118. if !new_npm_packages.is_empty() {
  119. new_npm_packages.sort();
  120. new_npm_packages.dedup();
  121. pm.install(&new_npm_packages, app_dir)
  122. .context("Error installing new npm packages")?;
  123. }
  124. Ok(new_plugins)
  125. }
  126. fn migrate_imports<'a>(
  127. path: &'a Path,
  128. js_source: &'a str,
  129. new_plugins: &mut Vec<String>,
  130. npm_packages_to_remove: &mut Vec<String>,
  131. ) -> crate::Result<String> {
  132. let mut magic_js_source = MagicString::new(js_source);
  133. let has_partial_js = path
  134. .extension()
  135. .map_or(false, |ext| ext == "vue" || ext == "svelte");
  136. let sources = if !has_partial_js {
  137. vec![(SourceType::from_path(path).unwrap(), js_source, 0i64)]
  138. } else {
  139. partial_loader::PartialLoader::parse(
  140. path
  141. .extension()
  142. .unwrap_or_default()
  143. .to_str()
  144. .unwrap_or_default(),
  145. js_source,
  146. )
  147. .unwrap()
  148. .into_iter()
  149. .map(|s| (s.source_type, s.source_text, s.start as i64))
  150. .collect()
  151. };
  152. for (source_type, js_source, script_start) in sources {
  153. let allocator = Allocator::default();
  154. let ret = Parser::new(&allocator, js_source, source_type).parse();
  155. if !ret.errors.is_empty() {
  156. anyhow::bail!(
  157. "failed to parse {} as valid Javascript/Typescript file",
  158. path.display()
  159. )
  160. }
  161. let mut program = ret.program;
  162. let mut stmts_to_add = Vec::new();
  163. let mut imports_to_add = Vec::new();
  164. for import in program.body.iter_mut() {
  165. if let Statement::ImportDeclaration(stmt) = import {
  166. let module = stmt.source.value.as_str();
  167. // convert module to its pluginfied module or renamed one
  168. // import { ... } from "@tauri-apps/api/window" -> import { ... } from "@tauri-apps/api/webviewWindow"
  169. // import { ... } from "@tauri-apps/api/cli" -> import { ... } from "@tauri-apps/plugin-cli"
  170. if let Some(&new_module) = MODULES_MAP.get(module) {
  171. // +1 and -1, to skip modifying the import quotes
  172. magic_js_source
  173. .overwrite(
  174. script_start + stmt.source.span.start as i64 + 1,
  175. script_start + stmt.source.span.end as i64 - 1,
  176. new_module,
  177. Default::default(),
  178. )
  179. .map_err(|e| anyhow::anyhow!("{e}"))
  180. .context("failed to replace import source")?;
  181. // if module was pluginified, add to packages
  182. if let Some(plugin_name) = new_module.strip_prefix("@tauri-apps/plugin-") {
  183. new_plugins.push(plugin_name.to_string());
  184. }
  185. // if the module is a v1 plugin, we should remove it
  186. if module.starts_with("tauri-plugin-") {
  187. npm_packages_to_remove.push(module.to_string());
  188. }
  189. }
  190. // skip parsing non @tauri-apps/api imports
  191. if !module.starts_with("@tauri-apps/api") {
  192. continue;
  193. }
  194. let Some(specifiers) = &mut stmt.specifiers else {
  195. continue;
  196. };
  197. for specifier in specifiers.iter() {
  198. if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
  199. let new_identifier = match specifier.imported.name().as_str() {
  200. // migrate appWindow from:
  201. // ```
  202. // import { appWindow } from "@tauri-apps/api/window"
  203. // ```
  204. // to:
  205. // ```
  206. // import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
  207. // const appWindow = getCurrentWebviewWindow()
  208. // ```
  209. "appWindow" if module == "@tauri-apps/api/window" => {
  210. stmts_to_add.push("\nconst appWindow = getCurrentWebviewWindow()");
  211. Some("getCurrentWebviewWindow")
  212. }
  213. // migrate pluginified modules from:
  214. // ```
  215. // import { dialog, cli as superCli } from "@tauri-apps/api"
  216. // ```
  217. // to:
  218. // ```
  219. // import * as dialog from "@tauri-apps/plugin-dialog"
  220. // import * as cli as superCli from "@tauri-apps/plugin-cli"
  221. // ```
  222. import if PLUGINIFIED_MODULES.contains(&import) && module == "@tauri-apps/api" => {
  223. let js_plugin: &str = MODULES_MAP[&format!("@tauri-apps/api/{import}")];
  224. let (_, plugin_name) = js_plugin.split_once("plugin-").unwrap();
  225. new_plugins.push(plugin_name.to_string());
  226. if specifier.local.name.as_str() != import {
  227. let local = &specifier.local.name;
  228. imports_to_add.push(format!(
  229. "\nimport * as {import} as {local} from \"{js_plugin}\""
  230. ));
  231. } else {
  232. imports_to_add.push(format!("\nimport * as {import} from \"{js_plugin}\""));
  233. };
  234. None
  235. }
  236. import if module == "@tauri-apps/api" => match RENAMED_MODULES.get(import) {
  237. Some(m) => Some(*m),
  238. None => continue,
  239. },
  240. // nothing to do, go to next specifier
  241. _ => continue,
  242. };
  243. // if identifier was renamed, it will be Some()
  244. // and so we convert the import
  245. // import { appWindow } from "@tauri-apps/api/window" -> import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
  246. if let Some(new_identifier) = new_identifier {
  247. magic_js_source
  248. .overwrite(
  249. script_start + specifier.span.start as i64,
  250. script_start + specifier.span.end as i64,
  251. new_identifier,
  252. Default::default(),
  253. )
  254. .map_err(|e| anyhow::anyhow!("{e}"))
  255. .context("failed to rename identifier")?;
  256. } else {
  257. // if None, we need to remove this specifier,
  258. // it will also be replaced with an import from its new plugin below
  259. // find the next comma or the bracket ending the import
  260. let start = specifier.span.start as usize;
  261. let sliced = &js_source[start..];
  262. let comma_or_bracket = sliced.chars().find_position(|&c| c == ',' || c == '}');
  263. let end = match comma_or_bracket {
  264. Some((n, ',')) => n + start + 1,
  265. Some((_, '}')) => specifier.span.end as _,
  266. _ => continue,
  267. };
  268. magic_js_source
  269. .remove(script_start + start as i64, script_start + end as i64)
  270. .map_err(|e| anyhow::anyhow!("{e}"))
  271. .context("failed to remove identifier")?;
  272. }
  273. }
  274. }
  275. }
  276. }
  277. // find the end of import list
  278. // fallback to the program start
  279. let start = program
  280. .body
  281. .iter()
  282. .rev()
  283. .find(|s| matches!(s, Statement::ImportDeclaration(_)))
  284. .map(|s| match s {
  285. Statement::ImportDeclaration(s) => s.span.end,
  286. _ => unreachable!(),
  287. })
  288. .unwrap_or(program.span.start);
  289. if !imports_to_add.is_empty() {
  290. for import in imports_to_add {
  291. magic_js_source
  292. .append_right(script_start as u32 + start, &import)
  293. .map_err(|e| anyhow::anyhow!("{e}"))
  294. .context("failed to add import")?;
  295. }
  296. }
  297. if !stmts_to_add.is_empty() {
  298. for stmt in stmts_to_add {
  299. magic_js_source
  300. .append_right(script_start as u32 + start, stmt)
  301. .map_err(|e| anyhow::anyhow!("{e}"))
  302. .context("failed to add statement")?;
  303. }
  304. }
  305. }
  306. Ok(magic_js_source.to_string())
  307. }
  308. #[cfg(test)]
  309. mod tests {
  310. use super::*;
  311. use pretty_assertions::assert_eq;
  312. #[test]
  313. fn migrates_vue() {
  314. let input = r#"
  315. <template>
  316. <div>Tauri!</div>
  317. </template>
  318. <script setup>
  319. import { useState } from "react";
  320. import reactLogo from "./assets/react.svg";
  321. import { invoke, dialog, cli as superCli } from "@tauri-apps/api";
  322. import { appWindow } from "@tauri-apps/api/window";
  323. import { convertFileSrc } from "@tauri-apps/api/tauri";
  324. import { open } from "@tauri-apps/api/dialog";
  325. import { register } from "@tauri-apps/api/globalShortcut";
  326. import clipboard from "@tauri-apps/api/clipboard";
  327. import * as fs from "@tauri-apps/api/fs";
  328. import "./App.css";
  329. </script>
  330. <style>
  331. .greeting {
  332. color: red;
  333. font-weight: bold;
  334. }
  335. </style>
  336. "#;
  337. let expected = r#"
  338. <template>
  339. <div>Tauri!</div>
  340. </template>
  341. <script setup>
  342. import { useState } from "react";
  343. import reactLogo from "./assets/react.svg";
  344. import { invoke, } from "@tauri-apps/api";
  345. import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
  346. import { convertFileSrc } from "@tauri-apps/api/core";
  347. import { open } from "@tauri-apps/plugin-dialog";
  348. import { register } from "@tauri-apps/plugin-global-shortcut";
  349. import clipboard from "@tauri-apps/plugin-clipboard-manager";
  350. import * as fs from "@tauri-apps/plugin-fs";
  351. import "./App.css";
  352. import * as dialog from "@tauri-apps/plugin-dialog"
  353. import * as cli as superCli from "@tauri-apps/plugin-cli"
  354. const appWindow = getCurrentWebviewWindow()
  355. </script>
  356. <style>
  357. .greeting {
  358. color: red;
  359. font-weight: bold;
  360. }
  361. </style>
  362. "#;
  363. let mut new_plugins = Vec::new();
  364. let mut npm_packages_to_remove = Vec::new();
  365. let migrated = migrate_imports(
  366. Path::new("file.vue"),
  367. input,
  368. &mut new_plugins,
  369. &mut npm_packages_to_remove,
  370. )
  371. .unwrap();
  372. assert_eq!(migrated, expected);
  373. assert_eq!(
  374. new_plugins,
  375. vec![
  376. "dialog",
  377. "cli",
  378. "dialog",
  379. "global-shortcut",
  380. "clipboard-manager",
  381. "fs"
  382. ]
  383. );
  384. assert_eq!(npm_packages_to_remove, Vec::<String>::new());
  385. }
  386. #[test]
  387. fn migrates_svelte() {
  388. let input = r#"
  389. <form>
  390. </form>
  391. <script>
  392. import { useState } from "react";
  393. import reactLogo from "./assets/react.svg";
  394. import { invoke, dialog, cli as superCli } from "@tauri-apps/api";
  395. import { appWindow } from "@tauri-apps/api/window";
  396. import { convertFileSrc } from "@tauri-apps/api/tauri";
  397. import { open } from "@tauri-apps/api/dialog";
  398. import { register } from "@tauri-apps/api/globalShortcut";
  399. import clipboard from "@tauri-apps/api/clipboard";
  400. import * as fs from "@tauri-apps/api/fs";
  401. import "./App.css";
  402. </script>
  403. "#;
  404. let expected = r#"
  405. <form>
  406. </form>
  407. <script>
  408. import { useState } from "react";
  409. import reactLogo from "./assets/react.svg";
  410. import { invoke, } from "@tauri-apps/api";
  411. import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
  412. import { convertFileSrc } from "@tauri-apps/api/core";
  413. import { open } from "@tauri-apps/plugin-dialog";
  414. import { register } from "@tauri-apps/plugin-global-shortcut";
  415. import clipboard from "@tauri-apps/plugin-clipboard-manager";
  416. import * as fs from "@tauri-apps/plugin-fs";
  417. import "./App.css";
  418. import * as dialog from "@tauri-apps/plugin-dialog"
  419. import * as cli as superCli from "@tauri-apps/plugin-cli"
  420. const appWindow = getCurrentWebviewWindow()
  421. </script>
  422. "#;
  423. let mut new_plugins = Vec::new();
  424. let mut npm_packages_to_remove = Vec::new();
  425. let migrated = migrate_imports(
  426. Path::new("file.svelte"),
  427. input,
  428. &mut new_plugins,
  429. &mut npm_packages_to_remove,
  430. )
  431. .unwrap();
  432. assert_eq!(migrated, expected);
  433. assert_eq!(
  434. new_plugins,
  435. vec![
  436. "dialog",
  437. "cli",
  438. "dialog",
  439. "global-shortcut",
  440. "clipboard-manager",
  441. "fs"
  442. ]
  443. );
  444. assert_eq!(npm_packages_to_remove, Vec::<String>::new());
  445. }
  446. #[test]
  447. fn migrates_js() {
  448. let input = r#"
  449. import { useState } from "react";
  450. import reactLogo from "./assets/react.svg";
  451. import { invoke, dialog, cli as superCli } from "@tauri-apps/api";
  452. import { appWindow } from "@tauri-apps/api/window";
  453. import { convertFileSrc } from "@tauri-apps/api/tauri";
  454. import { open } from "@tauri-apps/api/dialog";
  455. import { register } from "@tauri-apps/api/globalShortcut";
  456. import clipboard from "@tauri-apps/api/clipboard";
  457. import * as fs from "@tauri-apps/api/fs";
  458. import { Store } from "tauri-plugin-store-api";
  459. import Database from "tauri-plugin-sql-api";
  460. import "./App.css";
  461. function App() {
  462. const [greetMsg, setGreetMsg] = useState("");
  463. const [name, setName] = useState("");
  464. async function greet() {
  465. // Learn more about Tauri commands at https://v2.tauri.app/develop/calling-rust/#commands
  466. setGreetMsg(await invoke("greet", { name }));
  467. await open();
  468. await dialog.save();
  469. await convertFileSrc("");
  470. const a = appWindow.label;
  471. superCli.getMatches();
  472. clipboard.readText();
  473. fs.exists("");
  474. }
  475. return (
  476. <div className="container">
  477. <h1>Welcome to Tauri!</h1>
  478. <div className="row">
  479. <a href="https://vitejs.dev" target="_blank">
  480. <img src="/vite.svg" className="logo vite" alt="Vite logo" />
  481. </a>
  482. <a href="https://tauri.app" target="_blank">
  483. <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
  484. </a>
  485. <a href="https://reactjs.org" target="_blank">
  486. <img src={reactLogo} className="logo react" alt="React logo" />
  487. </a>
  488. </div>
  489. <p>Click on the Tauri, Vite, and React logos to learn more.</p>
  490. <form
  491. className="row"
  492. onSubmit={(e) => {
  493. e.preventDefault();
  494. greet();
  495. }}
  496. >
  497. <input
  498. id="greet-input"
  499. onChange={(e) => setName(e.currentTarget.value)}
  500. placeholder="Enter a name..."
  501. />
  502. <button type="submit">Greet</button>
  503. </form>
  504. <p>{greetMsg}</p>
  505. </div>
  506. );
  507. }
  508. export default App;
  509. "#;
  510. let expected = r#"
  511. import { useState } from "react";
  512. import reactLogo from "./assets/react.svg";
  513. import { invoke, } from "@tauri-apps/api";
  514. import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
  515. import { convertFileSrc } from "@tauri-apps/api/core";
  516. import { open } from "@tauri-apps/plugin-dialog";
  517. import { register } from "@tauri-apps/plugin-global-shortcut";
  518. import clipboard from "@tauri-apps/plugin-clipboard-manager";
  519. import * as fs from "@tauri-apps/plugin-fs";
  520. import { Store } from "@tauri-apps/plugin-store";
  521. import Database from "@tauri-apps/plugin-sql";
  522. import "./App.css";
  523. import * as dialog from "@tauri-apps/plugin-dialog"
  524. import * as cli as superCli from "@tauri-apps/plugin-cli"
  525. const appWindow = getCurrentWebviewWindow()
  526. function App() {
  527. const [greetMsg, setGreetMsg] = useState("");
  528. const [name, setName] = useState("");
  529. async function greet() {
  530. // Learn more about Tauri commands at https://v2.tauri.app/develop/calling-rust/#commands
  531. setGreetMsg(await invoke("greet", { name }));
  532. await open();
  533. await dialog.save();
  534. await convertFileSrc("");
  535. const a = appWindow.label;
  536. superCli.getMatches();
  537. clipboard.readText();
  538. fs.exists("");
  539. }
  540. return (
  541. <div className="container">
  542. <h1>Welcome to Tauri!</h1>
  543. <div className="row">
  544. <a href="https://vitejs.dev" target="_blank">
  545. <img src="/vite.svg" className="logo vite" alt="Vite logo" />
  546. </a>
  547. <a href="https://tauri.app" target="_blank">
  548. <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
  549. </a>
  550. <a href="https://reactjs.org" target="_blank">
  551. <img src={reactLogo} className="logo react" alt="React logo" />
  552. </a>
  553. </div>
  554. <p>Click on the Tauri, Vite, and React logos to learn more.</p>
  555. <form
  556. className="row"
  557. onSubmit={(e) => {
  558. e.preventDefault();
  559. greet();
  560. }}
  561. >
  562. <input
  563. id="greet-input"
  564. onChange={(e) => setName(e.currentTarget.value)}
  565. placeholder="Enter a name..."
  566. />
  567. <button type="submit">Greet</button>
  568. </form>
  569. <p>{greetMsg}</p>
  570. </div>
  571. );
  572. }
  573. export default App;
  574. "#;
  575. let mut new_plugins = Vec::new();
  576. let mut npm_packages_to_remove = Vec::new();
  577. let migrated = migrate_imports(
  578. Path::new("file.js"),
  579. input,
  580. &mut new_plugins,
  581. &mut npm_packages_to_remove,
  582. )
  583. .unwrap();
  584. assert_eq!(migrated, expected);
  585. assert_eq!(
  586. new_plugins,
  587. vec![
  588. "dialog",
  589. "cli",
  590. "dialog",
  591. "global-shortcut",
  592. "clipboard-manager",
  593. "fs",
  594. "store",
  595. "sql"
  596. ]
  597. );
  598. assert_eq!(
  599. npm_packages_to_remove,
  600. vec!["tauri-plugin-store-api", "tauri-plugin-sql-api"]
  601. );
  602. }
  603. }