Browse Source

feat(cli): add migrate command (#7008)

Lucas Fernandes Nogueira 2 years ago
parent
commit
b0f947752a

+ 6 - 0
.changes/migrate-cmd.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Added `migrate` command.

+ 50 - 3
tooling/cli/Cargo.lock

@@ -3843,7 +3843,7 @@ dependencies = [
  "strsim",
  "tar",
  "tauri-icns",
- "tauri-utils",
+ "tauri-utils 2.0.0-alpha.5",
  "tempfile",
  "thiserror",
  "time",
@@ -3902,7 +3902,8 @@ dependencies = [
  "tauri-bundler",
  "tauri-icns",
  "tauri-mobile",
- "tauri-utils",
+ "tauri-utils 1.3.0",
+ "tauri-utils 2.0.0-alpha.5",
  "textwrap",
  "thiserror",
  "tokio",
@@ -3976,6 +3977,35 @@ dependencies = [
  "windows 0.39.0",
 ]
 
+[[package]]
+name = "tauri-utils"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
+dependencies = [
+ "aes-gcm",
+ "ctor 0.1.26",
+ "getrandom 0.2.9",
+ "heck",
+ "html5ever",
+ "infer",
+ "json-patch 1.0.0",
+ "json5",
+ "kuchiki",
+ "memchr",
+ "phf 0.10.1",
+ "schemars",
+ "semver",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "serialize-to-javascript",
+ "thiserror",
+ "toml",
+ "url",
+ "windows 0.39.0",
+]
+
 [[package]]
 name = "tauri-utils"
 version = "2.0.0-alpha.5"
@@ -4701,6 +4731,7 @@ version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
 dependencies = [
+ "windows-implement 0.39.0",
  "windows_aarch64_msvc 0.39.0",
  "windows_i686_gnu 0.39.0",
  "windows_i686_msvc 0.39.0",
@@ -4714,7 +4745,7 @@ version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
 dependencies = [
- "windows-implement",
+ "windows-implement 0.44.0",
  "windows-interface",
  "windows-targets 0.42.2",
 ]
@@ -4728,6 +4759,16 @@ dependencies = [
  "windows-targets 0.48.0",
 ]
 
+[[package]]
+name = "windows-implement"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7"
+dependencies = [
+ "syn 1.0.109",
+ "windows-tokens",
+]
+
 [[package]]
 name = "windows-implement"
 version = "0.44.0"
@@ -4813,6 +4854,12 @@ dependencies = [
  "windows_x86_64_msvc 0.48.0",
 ]
 
+[[package]]
+name = "windows-tokens"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.2"

+ 1 - 0
tooling/cli/Cargo.toml

@@ -61,6 +61,7 @@ duct = "0.13"
 toml_edit = "0.14"
 json-patch = "0.2"
 tauri-utils = { version = "2.0.0-alpha.5", path = "../../core/tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
+tauri-utils-v1 = { version = "1", package = "tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
 toml = "0.5"
 jsonschema = "0.16"
 handlebars = "4.3"

+ 1 - 1
tooling/cli/src/interface/rust/manifest.rs

@@ -98,7 +98,7 @@ pub fn read_manifest(manifest_path: &Path) -> crate::Result<Document> {
   Ok(manifest)
 }
 
-fn toml_array(features: &HashSet<String>) -> Array {
+pub fn toml_array(features: &HashSet<String>) -> Array {
   let mut f = Array::default();
   let mut features: Vec<String> = features.iter().map(|f| f.to_string()).collect();
   features.sort();

+ 4 - 0
tooling/cli/src/lib.rs

@@ -11,6 +11,7 @@ mod icon;
 mod info;
 mod init;
 mod interface;
+mod migrate;
 mod mobile;
 mod plugin;
 mod signer;
@@ -96,6 +97,8 @@ enum Commands {
   Android(mobile::android::Cli),
   #[cfg(target_os = "macos")]
   Ios(mobile::ios::Cli),
+  /// Migrate from v1 to v2
+  Migrate,
 }
 
 fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
@@ -198,6 +201,7 @@ where
     Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
     #[cfg(target_os = "macos")]
     Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
+    Commands::Migrate => migrate::command()?,
   }
 
   Ok(())

+ 272 - 0
tooling/cli/src/migrate/config.rs

@@ -0,0 +1,272 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::Result;
+
+use serde_json::{Map, Value};
+
+use std::{fs::write, path::Path};
+
+macro_rules! move_allowlist_object {
+  ($plugins: ident, $value: expr, $plugin: literal, $field: literal) => {{
+    if $value != Default::default() {
+      $plugins
+        .entry($plugin)
+        .or_insert_with(|| Value::Object(Default::default()))
+        .as_object_mut()
+        .unwrap()
+        .insert($field.into(), serde_json::to_value($value)?);
+    }
+  }};
+}
+
+pub fn migrate(tauri_dir: &Path) -> Result<()> {
+  if let Ok((mut config, config_path)) =
+    tauri_utils_v1::config::parse::parse_value(tauri_dir.join("tauri.conf.json"))
+  {
+    migrate_config(&mut config)?;
+    write(config_path, serde_json::to_string_pretty(&config)?)?;
+  }
+
+  Ok(())
+}
+
+fn migrate_config(config: &mut Value) -> Result<()> {
+  if let Some(config) = config.as_object_mut() {
+    let mut plugins = config
+      .entry("plugins")
+      .or_insert_with(|| Value::Object(Default::default()))
+      .as_object_mut()
+      .unwrap()
+      .clone();
+
+    if let Some(tauri_config) = config.get_mut("tauri").and_then(|c| c.as_object_mut()) {
+      // allowlist
+      if let Some(allowlist) = tauri_config.remove("allowlist") {
+        process_allowlist(tauri_config, &mut plugins, allowlist)?;
+      }
+
+      // cli
+      if let Some(cli) = tauri_config.remove("cli") {
+        process_cli(&mut plugins, cli)?;
+      }
+
+      // cli
+      if let Some(updater) = tauri_config.remove("updater") {
+        process_updater(tauri_config, &mut plugins, updater)?;
+      }
+    }
+
+    config.insert("plugins".into(), plugins.into());
+  }
+
+  Ok(())
+}
+
+fn process_allowlist(
+  tauri_config: &mut Map<String, Value>,
+  plugins: &mut Map<String, Value>,
+  allowlist: Value,
+) -> Result<()> {
+  let allowlist: tauri_utils_v1::config::AllowlistConfig = serde_json::from_value(allowlist)?;
+
+  move_allowlist_object!(plugins, allowlist.fs.scope, "fs", "scope");
+  move_allowlist_object!(plugins, allowlist.shell.scope, "shell", "scope");
+  move_allowlist_object!(plugins, allowlist.shell.open, "shell", "open");
+  move_allowlist_object!(plugins, allowlist.http.scope, "http", "scope");
+
+  if allowlist.protocol.asset_scope != Default::default() {
+    let security = tauri_config
+      .entry("security")
+      .or_insert_with(|| Value::Object(Default::default()))
+      .as_object_mut()
+      .unwrap();
+
+    let mut asset_protocol = Map::new();
+    asset_protocol.insert(
+      "scope".into(),
+      serde_json::to_value(allowlist.protocol.asset_scope)?,
+    );
+    if allowlist.protocol.asset {
+      asset_protocol.insert("enable".into(), true.into());
+    }
+    security.insert("assetProtocol".into(), asset_protocol.into());
+  }
+
+  Ok(())
+}
+
+fn process_cli(plugins: &mut Map<String, Value>, cli: Value) -> Result<()> {
+  if let Some(cli) = cli.as_object() {
+    plugins.insert("cli".into(), serde_json::to_value(cli)?);
+  }
+  Ok(())
+}
+
+fn process_updater(
+  tauri_config: &mut Map<String, Value>,
+  plugins: &mut Map<String, Value>,
+  mut updater: Value,
+) -> Result<()> {
+  if let Some(updater) = updater.as_object_mut() {
+    updater.remove("dialog");
+
+    let endpoints = updater
+      .remove("endpoints")
+      .unwrap_or_else(|| Value::Array(Default::default()));
+
+    let mut plugin_updater_config = Map::new();
+    plugin_updater_config.insert("endpoints".into(), endpoints);
+    if let Some(windows) = updater.get_mut("windows").and_then(|w| w.as_object_mut()) {
+      if let Some(installer_args) = windows.remove("installerArgs") {
+        let mut windows_updater_config = Map::new();
+        windows_updater_config.insert("installerArgs".into(), installer_args);
+
+        plugin_updater_config.insert("windows".into(), windows_updater_config.into());
+      }
+    }
+
+    plugins.insert("updater".into(), plugin_updater_config.into());
+  }
+
+  tauri_config
+    .get_mut("bundle")
+    .unwrap()
+    .as_object_mut()
+    .unwrap()
+    .insert("updater".into(), updater);
+
+  Ok(())
+}
+
+#[cfg(test)]
+mod test {
+  #[test]
+  fn migrate() {
+    let original = serde_json::json!({
+      "tauri": {
+        "bundle": {
+          "identifier": "com.tauri.test"
+        },
+        "cli": {
+          "description": "Tauri TEST"
+        },
+        "updater": {
+          "active": true,
+          "dialog": false,
+          "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
+          "endpoints": [
+            "https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
+          ],
+          "windows": {
+            "installerArgs": [],
+            "installMode": "passive"
+          }
+        },
+        "allowlist": {
+          "all": true,
+          "fs": {
+            "scope": {
+              "allow": ["$APPDATA/db/**", "$DOWNLOAD/**", "$RESOURCE/**"],
+              "deny": ["$APPDATA/db/*.stronghold"]
+            }
+          },
+          "shell": {
+            "open": true,
+            "scope": [
+              {
+                "name": "sh",
+                "cmd": "sh",
+                "args": ["-c", { "validator": "\\S+" }],
+                "sidecar": false
+              },
+              {
+                "name": "cmd",
+                "cmd": "cmd",
+                "args": ["/C", { "validator": "\\S+" }],
+                "sidecar": false
+              }
+            ]
+          },
+          "protocol": {
+            "asset": true,
+            "assetScope": {
+              "allow": ["$APPDATA/db/**", "$RESOURCE/**"],
+              "deny": ["$APPDATA/db/*.stronghold"]
+            }
+          },
+          "http": {
+            "scope": ["http://localhost:3003/"]
+          }
+        }
+      }
+    });
+
+    let mut migrated = original.clone();
+    super::migrate_config(&mut migrated).expect("failed to migrate config");
+
+    if let Err(e) = serde_json::from_value::<tauri_utils::config::Config>(migrated.clone()) {
+      panic!("migrated config is not valid: {e}");
+    }
+
+    // bundle > updater
+    assert_eq!(
+      migrated["tauri"]["bundle"]["updater"]["active"],
+      original["tauri"]["updater"]["active"]
+    );
+    assert_eq!(
+      migrated["tauri"]["bundle"]["updater"]["pubkey"],
+      original["tauri"]["updater"]["pubkey"]
+    );
+    assert_eq!(
+      migrated["tauri"]["bundle"]["updater"]["windows"]["installMode"],
+      original["tauri"]["updater"]["windows"]["installMode"]
+    );
+
+    // plugins > updater
+    assert_eq!(
+      migrated["plugins"]["updater"]["endpoints"],
+      original["tauri"]["updater"]["endpoints"]
+    );
+    assert_eq!(
+      migrated["plugins"]["updater"]["windows"]["installerArgs"],
+      original["tauri"]["updater"]["windows"]["installerArgs"]
+    );
+
+    // cli
+    assert_eq!(migrated["plugins"]["cli"], original["tauri"]["cli"]);
+
+    // fs scope
+    assert_eq!(
+      migrated["plugins"]["fs"]["scope"],
+      original["tauri"]["allowlist"]["fs"]["scope"]
+    );
+
+    // shell scope
+    assert_eq!(
+      migrated["plugins"]["shell"]["scope"],
+      original["tauri"]["allowlist"]["shell"]["scope"]
+    );
+    assert_eq!(
+      migrated["plugins"]["shell"]["open"],
+      original["tauri"]["allowlist"]["shell"]["open"]
+    );
+
+    // http scope
+    assert_eq!(
+      migrated["plugins"]["http"]["scope"],
+      original["tauri"]["allowlist"]["http"]["scope"]
+    );
+
+    // asset scope
+    assert_eq!(
+      migrated["tauri"]["security"]["assetProtocol"]["enable"],
+      original["tauri"]["allowlist"]["protocol"]["asset"]
+    );
+    assert_eq!(
+      migrated["tauri"]["security"]["assetProtocol"]["scope"],
+      original["tauri"]["allowlist"]["protocol"]["assetScope"]
+    );
+  }
+}

+ 327 - 0
tooling/cli/src/migrate/manifest.rs

@@ -0,0 +1,327 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{interface::rust::manifest::read_manifest, Result};
+
+use anyhow::Context;
+use itertools::Itertools;
+use tauri_utils_v1::config::Allowlist;
+use toml_edit::{Document, Entry, Item, Table, TableLike, Value};
+
+use std::{fs::File, io::Write, path::Path};
+
+const CRATE_TYPES: &[&str] = &["staticlib", "cdylib", "rlib"];
+
+pub fn migrate(tauri_dir: &Path) -> Result<()> {
+  let manifest_path = tauri_dir.join("Cargo.toml");
+  let mut manifest = read_manifest(&manifest_path)?;
+  migrate_manifest(&mut manifest)?;
+
+  let mut manifest_file =
+    File::create(&manifest_path).with_context(|| "failed to open Cargo.toml for rewrite")?;
+  manifest_file.write_all(
+    manifest
+      .to_string()
+      // apply some formatting fixes
+      .replace(r#"" ,features =["#, r#"", features = ["#)
+      .replace(r#"" , features"#, r#"", features"#)
+      .replace("]}", "] }")
+      .replace("={", "= {")
+      .replace("=[", "= [")
+      .as_bytes(),
+  )?;
+  manifest_file.flush()?;
+
+  Ok(())
+}
+
+fn migrate_manifest(manifest: &mut Document) -> Result<()> {
+  let dependencies = manifest
+    .as_table_mut()
+    .entry("dependencies")
+    .or_insert(Item::Table(Table::new()))
+    .as_table_mut()
+    .expect("manifest dependencies isn't a table");
+
+  let version = dependency_version();
+  migrate_dependency(dependencies, "tauri", version, &features_to_remove());
+
+  let lib = manifest
+    .as_table_mut()
+    .entry("lib")
+    .or_insert(Item::Table(Table::new()))
+    .as_table_mut()
+    .expect("manifest lib isn't a table");
+  match lib.entry("crate-type") {
+    Entry::Occupied(mut e) => {
+      if let Item::Value(Value::Array(types)) = e.get_mut() {
+        let mut crate_types_to_add = CRATE_TYPES.to_vec();
+        for t in types.iter() {
+          // type is already in the manifest, skip adding it
+          if let Some(i) = crate_types_to_add
+            .iter()
+            .position(|ty| Some(ty) == t.as_str().as_ref())
+          {
+            crate_types_to_add.remove(i);
+          }
+        }
+        for t in crate_types_to_add {
+          types.push(t);
+        }
+      }
+    }
+    Entry::Vacant(e) => {
+      let mut arr = toml_edit::Array::new();
+      arr.extend(CRATE_TYPES.to_vec());
+      e.insert(Item::Value(arr.into()));
+    }
+  }
+
+  Ok(())
+}
+
+fn features_to_remove() -> Vec<&'static str> {
+  let mut features_to_remove = tauri_utils_v1::config::AllowlistConfig::all_features();
+  features_to_remove.push("reqwest-client");
+  features_to_remove.push("reqwest-native-tls-vendored");
+  features_to_remove.push("process-command-api");
+  features_to_remove.push("shell-open-api");
+  features_to_remove.push("windows7-compat");
+  features_to_remove.push("updater");
+
+  // this allowlist feature was not removed
+  let index = features_to_remove
+    .iter()
+    .position(|x| x == &"protocol-asset")
+    .unwrap();
+  features_to_remove.remove(index);
+
+  features_to_remove
+}
+
+fn dependency_version() -> String {
+  let pre = env!("CARGO_PKG_VERSION_PRE");
+  if pre.is_empty() {
+    env!("CARGO_PKG_VERSION_MAJOR").to_string()
+  } else {
+    format!(
+      "{}.{}.{}-{}",
+      env!("CARGO_PKG_VERSION_MAJOR"),
+      env!("CARGO_PKG_VERSION_MINOR"),
+      env!("CARGO_PKG_VERSION_PATCH"),
+      pre.split('.').next().unwrap()
+    )
+  }
+}
+
+fn migrate_dependency(dependencies: &mut Table, name: &str, version: String, remove: &[&str]) {
+  let item = dependencies.entry(name).or_insert(Item::None);
+
+  // do not rewrite if dependency uses workspace inheritance
+  if item
+    .get("workspace")
+    .and_then(|v| v.as_bool())
+    .unwrap_or_default()
+  {
+    log::info!("`{name}` dependency has workspace inheritance enabled. The features array won't be automatically rewritten. Remove features: [{}]", remove.iter().join(", "));
+    return;
+  }
+
+  if let Some(dep) = item.as_table_mut() {
+    migrate_dependency_table(dep, version, remove);
+  } else if let Some(Value::InlineTable(table)) = item.as_value_mut() {
+    migrate_dependency_table(table, version, remove);
+  } else if item.as_str().is_some() {
+    *item = Item::Value(version.into());
+  }
+}
+
+fn migrate_dependency_table<D: TableLike>(dep: &mut D, version: String, remove: &[&str]) {
+  *dep.entry("version").or_insert(Item::None) = Item::Value(version.into());
+  let manifest_features = dep.entry("features").or_insert(Item::None);
+  if let Some(features_array) = manifest_features.as_array_mut() {
+    // remove features that shouldn't be in the manifest anymore
+    let mut i = features_array.len();
+    let mut add_features = Vec::new();
+    while i != 0 {
+      let index = i - 1;
+      if let Some(f) = features_array.get(index).and_then(|f| f.as_str()) {
+        if remove.contains(&f) {
+          let f = f.to_string();
+          features_array.remove(index);
+          if f == "reqwest-native-tls-vendored" {
+            add_features.push("native-tls-vendored");
+          }
+        }
+      }
+      i -= 1;
+    }
+    for f in add_features {
+      features_array.push(f);
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use itertools::Itertools;
+
+  fn migrate_deps<F: FnOnce(&[&str]) -> String>(get_toml: F) {
+    let keep_features = vec!["isolation", "protocol-asset"];
+    let mut features = super::features_to_remove();
+    features.extend(keep_features.clone());
+    let toml = get_toml(&features);
+
+    let mut manifest = toml.parse::<toml_edit::Document>().expect("invalid toml");
+    super::migrate_manifest(&mut manifest).expect("failed to migrate manifest");
+
+    let dependencies = manifest
+      .as_table()
+      .get("dependencies")
+      .expect("missing manifest dependencies")
+      .as_table()
+      .expect("manifest dependencies isn't a table");
+
+    let tauri = dependencies
+      .get("tauri")
+      .expect("missing tauri dependency in manifest");
+
+    let tauri_table = if let Some(table) = tauri.as_table() {
+      table.clone()
+    } else if let Some(toml_edit::Value::InlineTable(table)) = tauri.as_value() {
+      table.clone().into_table()
+    } else if let Some(version) = tauri.as_str() {
+      // convert the value to a table for the assert logic below
+      let mut table = toml_edit::Table::new();
+      table.insert(
+        "version",
+        toml_edit::Item::Value(version.to_string().into()),
+      );
+      table.insert(
+        "features",
+        toml_edit::Item::Value(toml_edit::Value::Array(Default::default())),
+      );
+      table
+    } else {
+      panic!("unexpected tauri dependency format");
+    };
+
+    // assert version matches
+    let version = tauri_table
+      .get("version")
+      .expect("missing version")
+      .as_str()
+      .expect("version must be a string");
+    assert_eq!(version, super::dependency_version());
+
+    // assert features matches
+    let features = tauri_table
+      .get("features")
+      .expect("missing features")
+      .as_array()
+      .expect("features must be an array")
+      .clone();
+    if toml.contains("reqwest-native-tls-vendored") {
+      assert!(
+        features
+          .iter()
+          .any(|f| f.as_str().expect("feature must be a string") == "native-tls-vendored"),
+        "reqwest-native-tls-vendored was not replaced with native-tls-vendored"
+      );
+    }
+    for feature in features.iter() {
+      let feature = feature.as_str().expect("feature must be a string");
+      assert!(
+        keep_features.contains(&feature) || feature == "native-tls-vendored",
+        "feature {feature} should have been removed"
+      );
+    }
+  }
+
+  fn migrate_lib(toml: &str) {
+    let mut manifest = toml.parse::<toml_edit::Document>().expect("invalid toml");
+    super::migrate_manifest(&mut manifest).expect("failed to migrate manifest");
+
+    let lib = manifest
+      .as_table()
+      .get("lib")
+      .expect("missing manifest lib")
+      .as_table()
+      .expect("manifest lib isn't a table");
+
+    let crate_types = lib
+      .get("crate-type")
+      .expect("missing lib crate-type")
+      .as_array()
+      .expect("crate-type must be an array");
+    let mut not_added_crate_types = super::CRATE_TYPES.to_vec();
+    for t in crate_types {
+      let t = t.as_str().expect("crate-type must be a string");
+      if let Some(i) = not_added_crate_types.iter().position(|ty| ty == &t) {
+        not_added_crate_types.remove(i);
+      }
+    }
+    assert!(
+      not_added_crate_types.is_empty(),
+      "missing crate-type: {not_added_crate_types:?}"
+    );
+  }
+
+  #[test]
+  fn migrate_table() {
+    migrate_deps(|features| {
+      format!(
+        r#"
+    [dependencies]
+    tauri = {{ version = "1.0.0", features = [{}] }}
+"#,
+        features.iter().map(|f| format!("{:?}", f)).join(", ")
+      )
+    });
+  }
+
+  #[test]
+  fn migrate_inline_table() {
+    migrate_deps(|features| {
+      format!(
+        r#"
+    [dependencies.tauri]
+    version = "1.0.0"
+    features = [{}]
+"#,
+        features.iter().map(|f| format!("{:?}", f)).join(", ")
+      )
+    });
+  }
+
+  #[test]
+  fn migrate_str() {
+    migrate_deps(|_features| {
+      r#"
+    [dependencies]
+    tauri = "1.0.0"
+"#
+      .into()
+    })
+  }
+
+  #[test]
+  fn migrate_missing_lib() {
+    migrate_lib("[dependencies]");
+  }
+
+  #[test]
+  fn migrate_missing_crate_types() {
+    migrate_lib("[lib]");
+  }
+
+  #[test]
+  fn migrate_add_crate_types() {
+    migrate_lib(
+      r#"
+    [lib]
+    crate-type = ["something"]"#,
+    );
+  }
+}

+ 17 - 0
tooling/cli/src/migrate/mod.rs

@@ -0,0 +1,17 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{helpers::app_paths::tauri_dir, Result};
+
+mod config;
+mod manifest;
+
+pub fn command() -> Result<()> {
+  let tauri_dir = tauri_dir();
+
+  config::migrate(&tauri_dir)?;
+  manifest::migrate(&tauri_dir)?;
+
+  Ok(())
+}