|
@@ -7,12 +7,20 @@ use crate::{
|
|
|
Result,
|
|
|
};
|
|
|
use anyhow::Context;
|
|
|
+use itertools::Itertools;
|
|
|
+use magic_string::MagicString;
|
|
|
+use oxc_allocator::Allocator;
|
|
|
+use oxc_ast::ast::*;
|
|
|
+use oxc_parser::Parser;
|
|
|
+use oxc_span::SourceType;
|
|
|
|
|
|
use std::{fs, path::Path};
|
|
|
|
|
|
-// (from, to)
|
|
|
-const RENAMED_MODULES: &[(&str, &str)] = &[("tauri", "core"), ("window", "webviewWindow")];
|
|
|
-const PLUGINIFIED_MODULES: &[&str] = &[
|
|
|
+const RENAMED_MODULES: phf::Map<&str, &str> = phf::phf_map! {
|
|
|
+ "tauri" => "core",
|
|
|
+ "window" => "webviewWindow"
|
|
|
+};
|
|
|
+const PLUGINIFIED_MODULES: [&str; 11] = [
|
|
|
"cli",
|
|
|
"clipboard",
|
|
|
"dialog",
|
|
@@ -25,6 +33,24 @@ const PLUGINIFIED_MODULES: &[&str] = &[
|
|
|
"shell",
|
|
|
"updater",
|
|
|
];
|
|
|
+// (from, to)
|
|
|
+const MODULES_MAP: phf::Map<&str, &str> = phf::phf_map! {
|
|
|
+ // renamed
|
|
|
+ "@tauri-apps/api/tauri" => "@tauri-apps/api/core",
|
|
|
+ "@tauri-apps/api/window" => "@tauri-apps/api/webviewWindow",
|
|
|
+ // pluginified
|
|
|
+ "@tauri-apps/api/cli" => "@tauri-apps/plugin-cli",
|
|
|
+ "@tauri-apps/api/clipboard" => "@tauri-apps/plugin-clipboard-manager",
|
|
|
+ "@tauri-apps/api/dialog" => "@tauri-apps/plugin-dialog",
|
|
|
+ "@tauri-apps/api/fs" => "@tauri-apps/plugin-fs",
|
|
|
+ "@tauri-apps/api/globalShortcut" => "@tauri-apps/plugin-global-shortcut",
|
|
|
+ "@tauri-apps/api/http" => "@tauri-apps/plugin-http",
|
|
|
+ "@tauri-apps/api/notification" => "@tauri-apps/plugin-notification",
|
|
|
+ "@tauri-apps/api/os" => "@tauri-apps/plugin-os",
|
|
|
+ "@tauri-apps/api/process" => "@tauri-apps/plugin-process",
|
|
|
+ "@tauri-apps/api/shell" => "@tauri-apps/plugin-shell",
|
|
|
+ "@tauri-apps/api/updater" => "@tauri-apps/plugin-updater",
|
|
|
+};
|
|
|
const JS_EXTENSIONS: &[&str] = &["js", "mjs", "jsx", "ts", "mts", "tsx"];
|
|
|
|
|
|
/// Returns a list of paths that could not be migrated
|
|
@@ -37,45 +63,18 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
|
|
.next()
|
|
|
.unwrap_or(PackageManager::Npm);
|
|
|
|
|
|
- let tauri_api_import_regex = regex::bytes::Regex::new(r"@tauri-apps/api/(\w+)").unwrap();
|
|
|
-
|
|
|
for entry in walk_builder(app_dir).build().flatten() {
|
|
|
if entry.file_type().map(|t| t.is_file()).unwrap_or_default() {
|
|
|
let path = entry.path();
|
|
|
let ext = path.extension().unwrap_or_default();
|
|
|
if JS_EXTENSIONS.iter().any(|e| e == &ext) {
|
|
|
- let js_contents = fs::read(path)?;
|
|
|
-
|
|
|
- let new_contents =
|
|
|
- tauri_api_import_regex.replace_all(&js_contents, |cap: ®ex::bytes::Captures<'_>| {
|
|
|
- let module = cap.get(1).unwrap().as_bytes();
|
|
|
- let mut module = String::from_utf8_lossy(module).to_string();
|
|
|
- let original = cap.get(0).unwrap().as_bytes();
|
|
|
- let original = String::from_utf8_lossy(original).to_string();
|
|
|
-
|
|
|
- let new = if let Some((_, renamed_to)) =
|
|
|
- RENAMED_MODULES.iter().find(|(from, _to)| *from == module)
|
|
|
- {
|
|
|
- renamed_to.to_string()
|
|
|
- } else if PLUGINIFIED_MODULES.contains(&module.as_str()) {
|
|
|
- match module.as_str() {
|
|
|
- "clipboard" => module = String::from("clipboard-manager"),
|
|
|
- "globalShortcut" => module = String::from("global-shortcut"),
|
|
|
- _ => {}
|
|
|
- }
|
|
|
- let js_plugin = format!("@tauri-apps/plugin-{module}");
|
|
|
- let cargo_crate = format!("tauri-plugin-{module}");
|
|
|
- new_npm_packages.push(js_plugin.clone());
|
|
|
- new_cargo_packages.push(cargo_crate);
|
|
|
- js_plugin
|
|
|
- } else {
|
|
|
- return original;
|
|
|
- };
|
|
|
-
|
|
|
- log::info!("Replacing `{original}` with `{new}` on {}", path.display());
|
|
|
- new
|
|
|
- });
|
|
|
-
|
|
|
+ let js_contents = std::fs::read_to_string(path)?;
|
|
|
+ let new_contents = migrate_imports(
|
|
|
+ path,
|
|
|
+ &js_contents,
|
|
|
+ &mut new_cargo_packages,
|
|
|
+ &mut new_npm_packages,
|
|
|
+ )?;
|
|
|
if new_contents != js_contents {
|
|
|
fs::write(path, new_contents)
|
|
|
.with_context(|| format!("Error writing {}", path.display()))?;
|
|
@@ -84,11 +83,15 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ new_npm_packages.sort();
|
|
|
+ new_npm_packages.dedup();
|
|
|
if !new_npm_packages.is_empty() {
|
|
|
pm.install(&new_npm_packages)
|
|
|
.context("Error installing new npm packages")?;
|
|
|
}
|
|
|
|
|
|
+ new_cargo_packages.sort();
|
|
|
+ new_cargo_packages.dedup();
|
|
|
if !new_cargo_packages.is_empty() {
|
|
|
cargo::install(&new_cargo_packages, Some(tauri_dir))
|
|
|
.context("Error installing new Cargo packages")?;
|
|
@@ -96,3 +99,373 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
|
|
|
|
|
Ok(())
|
|
|
}
|
|
|
+
|
|
|
+fn migrate_imports<'a>(
|
|
|
+ path: &'a Path,
|
|
|
+ js_source: &'a str,
|
|
|
+ new_cargo_packages: &mut Vec<String>,
|
|
|
+ new_npm_packages: &mut Vec<String>,
|
|
|
+) -> crate::Result<String> {
|
|
|
+ let mut magic_js_source = MagicString::new(js_source);
|
|
|
+
|
|
|
+ let source_type = SourceType::from_path(path).unwrap();
|
|
|
+ let allocator = Allocator::default();
|
|
|
+ let ret = Parser::new(&allocator, js_source, source_type).parse();
|
|
|
+ if !ret.errors.is_empty() {
|
|
|
+ anyhow::bail!(
|
|
|
+ "failed to parse {} as valid Javascript/Typescript file",
|
|
|
+ path.display()
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut program = ret.program;
|
|
|
+
|
|
|
+ let mut stmts_to_add = Vec::new();
|
|
|
+ let mut imports_to_add = Vec::new();
|
|
|
+
|
|
|
+ for import in program.body.iter_mut() {
|
|
|
+ if let Statement::ImportDeclaration(stmt) = import {
|
|
|
+ let module = stmt.source.value.as_str();
|
|
|
+
|
|
|
+ // skip parsing non @tauri-apps/api imports
|
|
|
+ if !module.starts_with("@tauri-apps/api") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // convert module to its pluginfied module or renamed one
|
|
|
+ // import { ... } from "@tauri-apps/api/window" -> import { ... } from "@tauri-apps/api/webviewWindow"
|
|
|
+ // import { ... } from "@tauri-apps/api/cli" -> import { ... } from "@tauri-apps/plugin-cli"
|
|
|
+ if let Some(&module) = MODULES_MAP.get(module) {
|
|
|
+ // +1 and -1, to skip modifying the import quotes
|
|
|
+ magic_js_source
|
|
|
+ .overwrite(
|
|
|
+ stmt.source.span.start as i64 + 1,
|
|
|
+ stmt.source.span.end as i64 - 1,
|
|
|
+ module,
|
|
|
+ Default::default(),
|
|
|
+ )
|
|
|
+ .map_err(|e| anyhow::anyhow!("{e}"))
|
|
|
+ .context("failed to replace import source")?;
|
|
|
+
|
|
|
+ // if module was pluginified, add to packages
|
|
|
+ let module = module.split_once("plugin-");
|
|
|
+ if let Some((_, module)) = module {
|
|
|
+ let js_plugin = format!("@tauri-apps/plugin-{module}");
|
|
|
+ let cargo_crate = format!("tauri-plugin-{module}");
|
|
|
+ new_npm_packages.push(js_plugin);
|
|
|
+ new_cargo_packages.push(cargo_crate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let Some(specifiers) = &mut stmt.specifiers else {
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+
|
|
|
+ for specifier in specifiers.iter() {
|
|
|
+ if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
|
|
|
+ let new_identifier = match specifier.imported.name().as_str() {
|
|
|
+ // migrate appWindow from:
|
|
|
+ // ```
|
|
|
+ // import { appWindow } from "@tauri-apps/api/window"
|
|
|
+ // ```
|
|
|
+ // to:
|
|
|
+ // ```
|
|
|
+ // import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
|
|
+ // const appWindow = getCurrentWebviewWindow()
|
|
|
+ // ```
|
|
|
+ "appWindow" if module == "@tauri-apps/api/window" => {
|
|
|
+ stmts_to_add.push("\nconst appWindow = getCurrentWebviewWindow()");
|
|
|
+ Some("getCurrentWebviewWindow")
|
|
|
+ }
|
|
|
+
|
|
|
+ // migrate pluginified modules from:
|
|
|
+ // ```
|
|
|
+ // import { dialog, cli as superCli } from "@tauri-apps/api"
|
|
|
+ // ```
|
|
|
+ // to:
|
|
|
+ // ```
|
|
|
+ // import dialog from "@tauri-apps/plugin-dialog"
|
|
|
+ // import cli as superCli from "@tauri-apps/plugin-cli"
|
|
|
+ // ```
|
|
|
+ import if PLUGINIFIED_MODULES.contains(&import) && module == "@tauri-apps/api" => {
|
|
|
+ let js_plugin: &str = MODULES_MAP[&format!("@tauri-apps/api/{import}")];
|
|
|
+ let (_, plugin_name) = js_plugin.split_once("plugin-").unwrap();
|
|
|
+ let cargo_crate = format!("tauri-plugin-{plugin_name}");
|
|
|
+ new_npm_packages.push(js_plugin.to_string());
|
|
|
+ new_cargo_packages.push(cargo_crate);
|
|
|
+
|
|
|
+ if specifier.local.name.as_str() != import {
|
|
|
+ let local = &specifier.local.name;
|
|
|
+ imports_to_add.push(format!("\nimport {import} as {local} from \"{js_plugin}\""));
|
|
|
+ } else {
|
|
|
+ imports_to_add.push(format!("\nimport {import} from \"{js_plugin}\""));
|
|
|
+ };
|
|
|
+ None
|
|
|
+ }
|
|
|
+
|
|
|
+ import if module == "@tauri-apps/api" => match RENAMED_MODULES.get(import) {
|
|
|
+ Some(m) => Some(*m),
|
|
|
+ None => continue,
|
|
|
+ },
|
|
|
+
|
|
|
+ // nothing to do, go to next specifier
|
|
|
+ _ => continue,
|
|
|
+ };
|
|
|
+
|
|
|
+ // if identifier was renamed, it will be Some()
|
|
|
+ // and so we convert the import
|
|
|
+ // import { appWindow } from "@tauri-apps/api/window" -> import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
|
|
+ if let Some(new_identifier) = new_identifier {
|
|
|
+ magic_js_source
|
|
|
+ .overwrite(
|
|
|
+ specifier.span.start as _,
|
|
|
+ specifier.span.end as _,
|
|
|
+ new_identifier,
|
|
|
+ Default::default(),
|
|
|
+ )
|
|
|
+ .map_err(|e| anyhow::anyhow!("{e}"))
|
|
|
+ .context("failed to rename identifier")?;
|
|
|
+ } else {
|
|
|
+ // if None, we need to remove this specifier,
|
|
|
+ // it will also be replaced with an import from its new plugin below
|
|
|
+
|
|
|
+ // find the next comma or the bracket ending the import
|
|
|
+ let start = specifier.span.start as usize;
|
|
|
+ let sliced = &js_source[start..];
|
|
|
+ let comma_or_bracket = sliced.chars().find_position(|&c| c == ',' || c == '}');
|
|
|
+ let end = match comma_or_bracket {
|
|
|
+ Some((n, ',')) => n + start + 1,
|
|
|
+ Some((_, '}')) => specifier.span.end as _,
|
|
|
+ _ => continue,
|
|
|
+ };
|
|
|
+
|
|
|
+ magic_js_source
|
|
|
+ .remove(start as _, end as _)
|
|
|
+ .map_err(|e| anyhow::anyhow!("{e}"))
|
|
|
+ .context("failed to remove identifier")?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // find the end of import list
|
|
|
+ // fallback to the program start
|
|
|
+ let start = program
|
|
|
+ .body
|
|
|
+ .iter()
|
|
|
+ .rev()
|
|
|
+ .find(|s| matches!(s, Statement::ImportDeclaration(_)))
|
|
|
+ .map(|s| match s {
|
|
|
+ Statement::ImportDeclaration(s) => s.span.end,
|
|
|
+ _ => unreachable!(),
|
|
|
+ })
|
|
|
+ .unwrap_or(program.span.start);
|
|
|
+
|
|
|
+ if !imports_to_add.is_empty() {
|
|
|
+ for import in imports_to_add {
|
|
|
+ magic_js_source
|
|
|
+ .append_right(start as _, &import)
|
|
|
+ .map_err(|e| anyhow::anyhow!("{e}"))
|
|
|
+ .context("failed to add import")?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !stmts_to_add.is_empty() {
|
|
|
+ for stmt in stmts_to_add {
|
|
|
+ magic_js_source
|
|
|
+ .append_right(start as _, stmt)
|
|
|
+ .map_err(|e| anyhow::anyhow!("{e}"))
|
|
|
+ .context("failed to add statement")?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(magic_js_source.to_string())
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn migrates() {
|
|
|
+ let input = r#"
|
|
|
+import { useState } from "react";
|
|
|
+import reactLogo from "./assets/react.svg";
|
|
|
+import { invoke, dialog, cli as superCli } from "@tauri-apps/api";
|
|
|
+import { appWindow } from "@tauri-apps/api/window";
|
|
|
+import { convertFileSrc } from "@tauri-apps/api/tauri";
|
|
|
+import { open } from "@tauri-apps/api/dialog";
|
|
|
+import { register } from "@tauri-apps/api/globalShortcut";
|
|
|
+import clipboard from "@tauri-apps/api/clipboard";
|
|
|
+import * as fs from "@tauri-apps/api/fs";
|
|
|
+import "./App.css";
|
|
|
+
|
|
|
+function App() {
|
|
|
+ const [greetMsg, setGreetMsg] = useState("");
|
|
|
+ const [name, setName] = useState("");
|
|
|
+
|
|
|
+ async function greet() {
|
|
|
+ // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
|
|
+ setGreetMsg(await invoke("greet", { name }));
|
|
|
+ await open();
|
|
|
+ await dialog.save();
|
|
|
+ await convertFileSrc("");
|
|
|
+ const a = appWindow.label;
|
|
|
+ superCli.getMatches();
|
|
|
+ clipboard.readText();
|
|
|
+ fs.exists("");
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="container">
|
|
|
+ <h1>Welcome to Tauri!</h1>
|
|
|
+
|
|
|
+ <div className="row">
|
|
|
+ <a href="https://vitejs.dev" target="_blank">
|
|
|
+ <img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
|
|
+ </a>
|
|
|
+ <a href="https://tauri.app" target="_blank">
|
|
|
+ <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
|
+ </a>
|
|
|
+ <a href="https://reactjs.org" target="_blank">
|
|
|
+ <img src={reactLogo} className="logo react" alt="React logo" />
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
|
|
+
|
|
|
+ <form
|
|
|
+ className="row"
|
|
|
+ onSubmit={(e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ greet();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ id="greet-input"
|
|
|
+ onChange={(e) => setName(e.currentTarget.value)}
|
|
|
+ placeholder="Enter a name..."
|
|
|
+ />
|
|
|
+ <button type="submit">Greet</button>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <p>{greetMsg}</p>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default App;
|
|
|
+"#;
|
|
|
+
|
|
|
+ let expected = r#"
|
|
|
+import { useState } from "react";
|
|
|
+import reactLogo from "./assets/react.svg";
|
|
|
+import { invoke, } from "@tauri-apps/api";
|
|
|
+import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
|
+import { convertFileSrc } from "@tauri-apps/api/core";
|
|
|
+import { open } from "@tauri-apps/plugin-dialog";
|
|
|
+import { register } from "@tauri-apps/plugin-global-shortcut";
|
|
|
+import clipboard from "@tauri-apps/plugin-clipboard-manager";
|
|
|
+import * as fs from "@tauri-apps/plugin-fs";
|
|
|
+import "./App.css";
|
|
|
+import dialog from "@tauri-apps/plugin-dialog"
|
|
|
+import cli as superCli from "@tauri-apps/plugin-cli"
|
|
|
+const appWindow = getCurrentWebviewWindow()
|
|
|
+
|
|
|
+function App() {
|
|
|
+ const [greetMsg, setGreetMsg] = useState("");
|
|
|
+ const [name, setName] = useState("");
|
|
|
+
|
|
|
+ async function greet() {
|
|
|
+ // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
|
|
+ setGreetMsg(await invoke("greet", { name }));
|
|
|
+ await open();
|
|
|
+ await dialog.save();
|
|
|
+ await convertFileSrc("");
|
|
|
+ const a = appWindow.label;
|
|
|
+ superCli.getMatches();
|
|
|
+ clipboard.readText();
|
|
|
+ fs.exists("");
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="container">
|
|
|
+ <h1>Welcome to Tauri!</h1>
|
|
|
+
|
|
|
+ <div className="row">
|
|
|
+ <a href="https://vitejs.dev" target="_blank">
|
|
|
+ <img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
|
|
+ </a>
|
|
|
+ <a href="https://tauri.app" target="_blank">
|
|
|
+ <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
|
+ </a>
|
|
|
+ <a href="https://reactjs.org" target="_blank">
|
|
|
+ <img src={reactLogo} className="logo react" alt="React logo" />
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
|
|
+
|
|
|
+ <form
|
|
|
+ className="row"
|
|
|
+ onSubmit={(e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ greet();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ id="greet-input"
|
|
|
+ onChange={(e) => setName(e.currentTarget.value)}
|
|
|
+ placeholder="Enter a name..."
|
|
|
+ />
|
|
|
+ <button type="submit">Greet</button>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <p>{greetMsg}</p>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default App;
|
|
|
+"#;
|
|
|
+
|
|
|
+ let mut new_cargo_packages = Vec::new();
|
|
|
+ let mut new_npm_packages = Vec::new();
|
|
|
+
|
|
|
+ let migrated = migrate_imports(
|
|
|
+ Path::new("file.js"),
|
|
|
+ input,
|
|
|
+ &mut new_cargo_packages,
|
|
|
+ &mut new_npm_packages,
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ assert_eq!(migrated, expected);
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ new_cargo_packages,
|
|
|
+ vec![
|
|
|
+ "tauri-plugin-dialog",
|
|
|
+ "tauri-plugin-cli",
|
|
|
+ "tauri-plugin-dialog",
|
|
|
+ "tauri-plugin-global-shortcut",
|
|
|
+ "tauri-plugin-clipboard-manager",
|
|
|
+ "tauri-plugin-fs"
|
|
|
+ ]
|
|
|
+ );
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ new_npm_packages,
|
|
|
+ vec![
|
|
|
+ "@tauri-apps/plugin-dialog",
|
|
|
+ "@tauri-apps/plugin-cli",
|
|
|
+ "@tauri-apps/plugin-dialog",
|
|
|
+ "@tauri-apps/plugin-global-shortcut",
|
|
|
+ "@tauri-apps/plugin-clipboard-manager",
|
|
|
+ "@tauri-apps/plugin-fs"
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|