mirror of https://github.com/tauri-apps/tauri
feat(cli): add support to Svelte and Vue.js code on v1 migration (#10544)
* feat(cli): add support to Svelte and Vue.js code on v1 migration * clippy
This commit is contained in:
parent
3998570fd3
commit
3bec7b1595
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"tauri-cli": patch:feat
|
||||
"@tauri-apps/cli": patch:feat
|
||||
---
|
||||
|
||||
v1 migrate script now migrates Svelte and Vue.js code.
|
|
@ -5228,6 +5228,7 @@ dependencies = [
|
|||
"local-ip-address",
|
||||
"log",
|
||||
"magic_string",
|
||||
"memchr",
|
||||
"minisign",
|
||||
"notify",
|
||||
"notify-debouncer-mini",
|
||||
|
|
|
@ -100,6 +100,7 @@ magic_string = "0.3"
|
|||
phf = { version = "0.11", features = ["macros"] }
|
||||
walkdir = "2"
|
||||
elf = "0.7"
|
||||
memchr = "2"
|
||||
|
||||
[target."cfg(windows)".dependencies.windows-sys]
|
||||
version = "0.52"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{path::Path, process::Command};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
|
@ -17,37 +17,6 @@ pub struct CargoInstallOptions<'a> {
|
|||
pub target: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn install(dependencies: &[String], cwd: Option<&Path>) -> crate::Result<()> {
|
||||
let dependencies_str = if dependencies.len() > 1 {
|
||||
"dependencies"
|
||||
} else {
|
||||
"dependency"
|
||||
};
|
||||
log::info!(
|
||||
"Installing Cargo {dependencies_str} {}...",
|
||||
dependencies
|
||||
.iter()
|
||||
.map(|d| format!("\"{d}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("add").args(dependencies);
|
||||
|
||||
if let Some(cwd) = cwd {
|
||||
cmd.current_dir(cwd);
|
||||
}
|
||||
|
||||
let status = cmd.status().with_context(|| "failed to run cargo")?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("Failed to install Cargo {dependencies_str}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> {
|
||||
let mut cargo = Command::new("cargo");
|
||||
cargo.arg("add");
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
helpers::{app_paths::walk_builder, cargo, npm::PackageManager},
|
||||
helpers::{app_paths::walk_builder, npm::PackageManager},
|
||||
Result,
|
||||
};
|
||||
use anyhow::Context;
|
||||
|
@ -16,6 +16,8 @@ use oxc_span::SourceType;
|
|||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
mod partial_loader;
|
||||
|
||||
const RENAMED_MODULES: phf::Map<&str, &str> = phf::phf_map! {
|
||||
"tauri" => "core",
|
||||
"window" => "webviewWindow"
|
||||
|
@ -51,12 +53,12 @@ const MODULES_MAP: phf::Map<&str, &str> = phf::phf_map! {
|
|||
"@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"];
|
||||
const JS_EXTENSIONS: &[&str] = &["js", "mjs", "jsx", "ts", "mts", "tsx", "svelte", "vue"];
|
||||
|
||||
/// Returns a list of paths that could not be migrated
|
||||
pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
||||
pub fn migrate(app_dir: &Path) -> Result<Vec<String>> {
|
||||
let mut new_npm_packages = Vec::new();
|
||||
let mut new_cargo_packages = Vec::new();
|
||||
let mut new_plugins = Vec::new();
|
||||
|
||||
let pre = env!("CARGO_PKG_VERSION_PRE");
|
||||
let npm_version = if pre.is_empty() {
|
||||
|
@ -92,12 +94,7 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
|||
let ext = path.extension().unwrap_or_default();
|
||||
if JS_EXTENSIONS.iter().any(|e| e == &ext) {
|
||||
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,
|
||||
)?;
|
||||
let new_contents = migrate_imports(path, &js_contents, &mut new_plugins)?;
|
||||
if new_contents != js_contents {
|
||||
fs::write(path, new_contents)
|
||||
.with_context(|| format!("Error writing {}", path.display()))?;
|
||||
|
@ -113,195 +110,205 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
|
|||
.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")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(new_plugins)
|
||||
}
|
||||
|
||||
fn migrate_imports<'a>(
|
||||
path: &'a Path,
|
||||
js_source: &'a str,
|
||||
new_cargo_packages: &mut Vec<String>,
|
||||
new_npm_packages: &mut Vec<String>,
|
||||
new_plugins: &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 has_partial_js = path
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "vue" || ext == "svelte");
|
||||
|
||||
let sources = if !has_partial_js {
|
||||
vec![(SourceType::from_path(path).unwrap(), js_source, 0i64)]
|
||||
} else {
|
||||
partial_loader::PartialLoader::parse(
|
||||
path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default(),
|
||||
js_source,
|
||||
)
|
||||
}
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| (s.source_type, s.source_text, s.start as i64))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut program = ret.program;
|
||||
for (source_type, js_source, script_start) in sources {
|
||||
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 stmts_to_add = Vec::new();
|
||||
let mut imports_to_add = Vec::new();
|
||||
let mut program = ret.program;
|
||||
|
||||
for import in program.body.iter_mut() {
|
||||
if let Statement::ImportDeclaration(stmt) = import {
|
||||
let module = stmt.source.value.as_str();
|
||||
let mut stmts_to_add = Vec::new();
|
||||
let mut imports_to_add = Vec::new();
|
||||
|
||||
// skip parsing non @tauri-apps/api imports
|
||||
if !module.starts_with("@tauri-apps/api") {
|
||||
continue;
|
||||
}
|
||||
for import in program.body.iter_mut() {
|
||||
if let Statement::ImportDeclaration(stmt) = import {
|
||||
let module = stmt.source.value.as_str();
|
||||
|
||||
// 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);
|
||||
// skip parsing non @tauri-apps/api imports
|
||||
if !module.starts_with("@tauri-apps/api") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(specifiers) = &mut stmt.specifiers else {
|
||||
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(
|
||||
script_start + stmt.source.span.start as i64 + 1,
|
||||
script_start + stmt.source.span.end as i64 - 1,
|
||||
module,
|
||||
Default::default(),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to replace import source")?;
|
||||
|
||||
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")
|
||||
}
|
||||
// if module was pluginified, add to packages
|
||||
let module = module.split_once("plugin-");
|
||||
if let Some((_, module)) = module {
|
||||
new_plugins.push(module.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// migrate pluginified modules from:
|
||||
// ```
|
||||
// import { dialog, cli as superCli } from "@tauri-apps/api"
|
||||
// ```
|
||||
// to:
|
||||
// ```
|
||||
// import * as dialog from "@tauri-apps/plugin-dialog"
|
||||
// import * as 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);
|
||||
let Some(specifiers) = &mut stmt.specifiers else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if specifier.local.name.as_str() != import {
|
||||
let local = &specifier.local.name;
|
||||
imports_to_add.push(format!(
|
||||
"\nimport * as {import} as {local} from \"{js_plugin}\""
|
||||
));
|
||||
} else {
|
||||
imports_to_add.push(format!("\nimport * as {import} from \"{js_plugin}\""));
|
||||
};
|
||||
None
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
import if module == "@tauri-apps/api" => match RENAMED_MODULES.get(import) {
|
||||
Some(m) => Some(*m),
|
||||
None => continue,
|
||||
},
|
||||
// migrate pluginified modules from:
|
||||
// ```
|
||||
// import { dialog, cli as superCli } from "@tauri-apps/api"
|
||||
// ```
|
||||
// to:
|
||||
// ```
|
||||
// import * as dialog from "@tauri-apps/plugin-dialog"
|
||||
// import * as 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();
|
||||
|
||||
// nothing to do, go to next specifier
|
||||
_ => continue,
|
||||
};
|
||||
new_plugins.push(plugin_name.to_string());
|
||||
|
||||
// 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
|
||||
if specifier.local.name.as_str() != import {
|
||||
let local = &specifier.local.name;
|
||||
imports_to_add.push(format!(
|
||||
"\nimport * as {import} as {local} from \"{js_plugin}\""
|
||||
));
|
||||
} else {
|
||||
imports_to_add.push(format!("\nimport * as {import} from \"{js_plugin}\""));
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
// 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 _,
|
||||
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,
|
||||
};
|
||||
|
||||
magic_js_source
|
||||
.remove(start as _, end as _)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to remove identifier")?;
|
||||
// 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(
|
||||
script_start + specifier.span.start as i64,
|
||||
script_start + specifier.span.end as i64,
|
||||
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(script_start + start as i64, script_start + end as i64)
|
||||
.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);
|
||||
// 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 !imports_to_add.is_empty() {
|
||||
for import in imports_to_add {
|
||||
magic_js_source
|
||||
.append_right(script_start as u32 + start, &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")?;
|
||||
if !stmts_to_add.is_empty() {
|
||||
for stmt in stmts_to_add {
|
||||
magic_js_source
|
||||
.append_right(script_start as u32 + start, stmt)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to add statement")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,7 +321,143 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn migrates() {
|
||||
fn migrates_vue() {
|
||||
let input = r#"
|
||||
<template>
|
||||
<div>Tauri!</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.greeting {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<template>
|
||||
<div>Tauri!</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 * as dialog from "@tauri-apps/plugin-dialog"
|
||||
import * as cli as superCli from "@tauri-apps/plugin-cli"
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.greeting {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
"#;
|
||||
|
||||
let mut new_plugins = Vec::new();
|
||||
|
||||
let migrated = migrate_imports(Path::new("file.vue"), input, &mut new_plugins).unwrap();
|
||||
|
||||
assert_eq!(migrated, expected);
|
||||
|
||||
assert_eq!(
|
||||
new_plugins,
|
||||
vec![
|
||||
"dialog",
|
||||
"cli",
|
||||
"dialog",
|
||||
"global-shortcut",
|
||||
"clipboard-manager",
|
||||
"fs"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrates_svelte() {
|
||||
let input = r#"
|
||||
<form>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
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";
|
||||
</script>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<form>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
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 * as dialog from "@tauri-apps/plugin-dialog"
|
||||
import * as cli as superCli from "@tauri-apps/plugin-cli"
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
</script>
|
||||
"#;
|
||||
|
||||
let mut new_plugins = Vec::new();
|
||||
|
||||
let migrated = migrate_imports(Path::new("file.svelte"), input, &mut new_plugins).unwrap();
|
||||
|
||||
assert_eq!(migrated, expected);
|
||||
|
||||
assert_eq!(
|
||||
new_plugins,
|
||||
vec![
|
||||
"dialog",
|
||||
"cli",
|
||||
"dialog",
|
||||
"global-shortcut",
|
||||
"clipboard-manager",
|
||||
"fs"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrates_js() {
|
||||
let input = r#"
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
|
@ -456,40 +599,21 @@ function App() {
|
|||
export default App;
|
||||
"#;
|
||||
|
||||
let mut new_cargo_packages = Vec::new();
|
||||
let mut new_npm_packages = Vec::new();
|
||||
let mut new_plugins = Vec::new();
|
||||
|
||||
let migrated = migrate_imports(
|
||||
Path::new("file.js"),
|
||||
input,
|
||||
&mut new_cargo_packages,
|
||||
&mut new_npm_packages,
|
||||
)
|
||||
.unwrap();
|
||||
let migrated = migrate_imports(Path::new("file.js"), input, &mut new_plugins).unwrap();
|
||||
|
||||
assert_eq!(migrated, expected);
|
||||
|
||||
assert_eq!(
|
||||
new_cargo_packages,
|
||||
new_plugins,
|
||||
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"
|
||||
"dialog",
|
||||
"cli",
|
||||
"dialog",
|
||||
"global-shortcut",
|
||||
"clipboard-manager",
|
||||
"fs"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// taken from https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/partial_loader/mod.rs
|
||||
|
||||
mod svelte;
|
||||
mod vue;
|
||||
|
||||
use oxc_span::SourceType;
|
||||
|
||||
pub use self::{svelte::SveltePartialLoader, vue::VuePartialLoader};
|
||||
|
||||
const SCRIPT_START: &str = "<script";
|
||||
const SCRIPT_END: &str = "</script>";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct JavaScriptSource<'a> {
|
||||
pub source_text: &'a str,
|
||||
pub source_type: SourceType,
|
||||
/// The javascript source could be embedded in some file,
|
||||
/// use `start` to record start offset of js block in the original file.
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
impl<'a> JavaScriptSource<'a> {
|
||||
pub fn new(source_text: &'a str, source_type: SourceType, start: usize) -> Self {
|
||||
Self {
|
||||
source_text,
|
||||
source_type,
|
||||
start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartialLoader;
|
||||
|
||||
impl PartialLoader {
|
||||
/// Extract js section of specifial files.
|
||||
/// Returns `None` if the specifial file does not have a js section.
|
||||
pub fn parse<'a>(ext: &str, source_text: &'a str) -> Option<Vec<JavaScriptSource<'a>>> {
|
||||
match ext {
|
||||
"vue" => Some(VuePartialLoader::new(source_text).parse()),
|
||||
"svelte" => Some(SveltePartialLoader::new(source_text).parse()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find closing angle for situations where there is another `>` in between.
|
||||
/// e.g. `<script generic="T extends Record<string, string>">`
|
||||
fn find_script_closing_angle(source_text: &str, pointer: usize) -> Option<usize> {
|
||||
let mut numbers_of_open_angle = 0;
|
||||
for (offset, c) in source_text[pointer..].char_indices() {
|
||||
match c {
|
||||
'>' => {
|
||||
if numbers_of_open_angle == 0 {
|
||||
return Some(offset);
|
||||
}
|
||||
numbers_of_open_angle -= 1;
|
||||
}
|
||||
'<' => {
|
||||
numbers_of_open_angle += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// taken from https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/partial_loader/svelte.rs
|
||||
|
||||
use memchr::memmem::Finder;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use super::{find_script_closing_angle, JavaScriptSource, SCRIPT_END, SCRIPT_START};
|
||||
|
||||
pub struct SveltePartialLoader<'a> {
|
||||
source_text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SveltePartialLoader<'a> {
|
||||
pub fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text }
|
||||
}
|
||||
|
||||
pub fn parse(self) -> Vec<JavaScriptSource<'a>> {
|
||||
self
|
||||
.parse_script()
|
||||
.map_or_else(Vec::new, |source| vec![source])
|
||||
}
|
||||
|
||||
fn parse_script(&self) -> Option<JavaScriptSource<'a>> {
|
||||
let script_start_finder = Finder::new(SCRIPT_START);
|
||||
let script_end_finder = Finder::new(SCRIPT_END);
|
||||
|
||||
let mut pointer = 0;
|
||||
|
||||
// find opening "<script"
|
||||
let offset = script_start_finder.find(self.source_text[pointer..].as_bytes())?;
|
||||
pointer += offset + SCRIPT_START.len();
|
||||
|
||||
// find closing ">"
|
||||
let offset = find_script_closing_angle(self.source_text, pointer)?;
|
||||
|
||||
// get lang="ts" attribute
|
||||
let content = &self.source_text[pointer..pointer + offset];
|
||||
let is_ts = content.contains("ts");
|
||||
|
||||
pointer += offset + 1;
|
||||
let js_start = pointer;
|
||||
|
||||
// find "</script>"
|
||||
let offset = script_end_finder.find(self.source_text[pointer..].as_bytes())?;
|
||||
let js_end = pointer + offset;
|
||||
|
||||
let source_text = &self.source_text[js_start..js_end];
|
||||
let source_type = SourceType::default()
|
||||
.with_module(true)
|
||||
.with_typescript(is_ts);
|
||||
Some(JavaScriptSource::new(source_text, source_type, js_start))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{JavaScriptSource, SveltePartialLoader};
|
||||
|
||||
fn parse_svelte(source_text: &str) -> JavaScriptSource<'_> {
|
||||
let sources = SveltePartialLoader::new(source_text).parse();
|
||||
*sources.first().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_svelte() {
|
||||
let source_text = r#"
|
||||
<script>
|
||||
console.log("hi");
|
||||
</script>
|
||||
<h1>Hello World</h1>
|
||||
"#;
|
||||
|
||||
let result = parse_svelte(source_text);
|
||||
assert_eq!(result.source_text.trim(), r#"console.log("hi");"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_svelte_ts_with_generic() {
|
||||
let source_text = r#"
|
||||
<script lang="ts" generics="T extends Record<string, unknown>">
|
||||
console.log("hi");
|
||||
</script>
|
||||
<h1>Hello World</h1>
|
||||
"#;
|
||||
|
||||
let result = parse_svelte(source_text);
|
||||
assert_eq!(result.source_text.trim(), r#"console.log("hi");"#);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// taken from https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/partial_loader/vue.rs
|
||||
|
||||
use memchr::memmem::Finder;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use super::{find_script_closing_angle, JavaScriptSource, SCRIPT_END, SCRIPT_START};
|
||||
|
||||
pub struct VuePartialLoader<'a> {
|
||||
source_text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> VuePartialLoader<'a> {
|
||||
pub fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text }
|
||||
}
|
||||
|
||||
pub fn parse(self) -> Vec<JavaScriptSource<'a>> {
|
||||
self.parse_scripts()
|
||||
}
|
||||
|
||||
/// Each *.vue file can contain at most
|
||||
/// * one `<script>` block (excluding `<script setup>`).
|
||||
/// * one `<script setup>` block (excluding normal `<script>`).
|
||||
/// <https://vuejs.org/api/sfc-spec.html#script>
|
||||
fn parse_scripts(&self) -> Vec<JavaScriptSource<'a>> {
|
||||
let mut pointer = 0;
|
||||
let Some(result1) = self.parse_script(&mut pointer) else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(result2) = self.parse_script(&mut pointer) else {
|
||||
return vec![result1];
|
||||
};
|
||||
vec![result1, result2]
|
||||
}
|
||||
|
||||
fn parse_script(&self, pointer: &mut usize) -> Option<JavaScriptSource<'a>> {
|
||||
let script_start_finder = Finder::new(SCRIPT_START);
|
||||
let script_end_finder = Finder::new(SCRIPT_END);
|
||||
|
||||
// find opening "<script"
|
||||
let offset = script_start_finder.find(self.source_text[*pointer..].as_bytes())?;
|
||||
*pointer += offset + SCRIPT_START.len();
|
||||
|
||||
// find closing ">"
|
||||
let offset = find_script_closing_angle(self.source_text, *pointer)?;
|
||||
|
||||
// get ts and jsx attribute
|
||||
let content = &self.source_text[*pointer..*pointer + offset];
|
||||
let is_ts = content.contains("ts");
|
||||
let is_jsx = content.contains("tsx") || content.contains("jsx");
|
||||
|
||||
*pointer += offset + 1;
|
||||
let js_start = *pointer;
|
||||
|
||||
// find "</script>"
|
||||
let offset = script_end_finder.find(self.source_text[*pointer..].as_bytes())?;
|
||||
let js_end = *pointer + offset;
|
||||
*pointer += offset + SCRIPT_END.len();
|
||||
|
||||
let source_text = &self.source_text[js_start..js_end];
|
||||
let source_type = SourceType::default()
|
||||
.with_module(true)
|
||||
.with_typescript(is_ts)
|
||||
.with_jsx(is_jsx);
|
||||
Some(JavaScriptSource::new(source_text, source_type, js_start))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{JavaScriptSource, VuePartialLoader};
|
||||
|
||||
fn parse_vue(source_text: &str) -> JavaScriptSource<'_> {
|
||||
let sources = VuePartialLoader::new(source_text).parse();
|
||||
*sources.first().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_vue_one_line() {
|
||||
let source_text = r#"
|
||||
<template>
|
||||
<h1>hello world</h1>
|
||||
</template>
|
||||
<script> console.log("hi") </script>
|
||||
"#;
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert_eq!(result.source_text, r#" console.log("hi") "#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vue_with_ts_flag_1() {
|
||||
let source_text = r#"
|
||||
<script lang="ts" setup generic="T extends Record<string, string>">
|
||||
1/1
|
||||
</script>
|
||||
"#;
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert!(result.source_type.is_typescript());
|
||||
assert_eq!(result.source_text.trim(), "1/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vue_with_ts_flag_2() {
|
||||
let source_text = r"
|
||||
<script lang=ts setup>
|
||||
1/1
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert!(result.source_type.is_typescript());
|
||||
assert_eq!(result.source_text.trim(), "1/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vue_with_ts_flag_3() {
|
||||
let source_text = r"
|
||||
<script lang='ts' setup>
|
||||
1/1
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert!(result.source_type.is_typescript());
|
||||
assert_eq!(result.source_text.trim(), "1/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vue_with_tsx_flag() {
|
||||
let source_text = r"
|
||||
<script lang=tsx setup>
|
||||
1/1
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert!(result.source_type.is_jsx());
|
||||
assert!(result.source_type.is_typescript());
|
||||
assert_eq!(result.source_text.trim(), "1/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vue_with_escape_string() {
|
||||
let source_text = r"
|
||||
<script setup>
|
||||
a.replace(/'/g, '\''))
|
||||
</script>
|
||||
<template> </template>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert!(!result.source_type.is_typescript());
|
||||
assert_eq!(result.source_text.trim(), r"a.replace(/'/g, '\''))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_level_template_literal() {
|
||||
let source_text = r"
|
||||
<script setup>
|
||||
`a${b( `c \`${d}\``)}`
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert_eq!(result.source_text.trim(), r"`a${b( `c \`${d}\``)}`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brace_with_regex_in_template_literal() {
|
||||
let source_text = r"
|
||||
<script setup>
|
||||
`${/{/}`
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert_eq!(result.source_text.trim(), r"`${/{/}`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_script() {
|
||||
let source_text = r"
|
||||
<template></template>
|
||||
";
|
||||
|
||||
let sources = VuePartialLoader::new(source_text).parse();
|
||||
assert!(sources.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_syntax_error() {
|
||||
let source_text = r"
|
||||
<script>
|
||||
console.log('error')
|
||||
";
|
||||
let sources = VuePartialLoader::new(source_text).parse();
|
||||
assert!(sources.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_scripts() {
|
||||
let source_text = r"
|
||||
<template></template>
|
||||
<script>a</script>
|
||||
<script setup>b</script>
|
||||
";
|
||||
let sources = VuePartialLoader::new(source_text).parse();
|
||||
assert_eq!(sources.len(), 2);
|
||||
assert_eq!(sources[0].source_text, "a");
|
||||
assert_eq!(sources[1].source_text, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode() {
|
||||
let source_text = r"
|
||||
<script setup>
|
||||
let 日历 = '2000年';
|
||||
const t = useTranslate({
|
||||
'zh-CN': {
|
||||
calendar: '日历',
|
||||
tiledDisplay: '平铺展示',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
";
|
||||
|
||||
let result = parse_vue(source_text);
|
||||
assert_eq!(
|
||||
result.source_text.trim(),
|
||||
"let 日历 = '2000年';
|
||||
const t = useTranslate({
|
||||
'zh-CN': {
|
||||
calendar: '日历',
|
||||
tiledDisplay: '平铺展示',
|
||||
},
|
||||
});"
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,9 +17,11 @@ pub fn run() -> Result<()> {
|
|||
let tauri_dir = tauri_dir();
|
||||
let app_dir = app_dir();
|
||||
|
||||
let migrated = config::migrate(tauri_dir).context("Could not migrate config")?;
|
||||
let mut migrated = config::migrate(tauri_dir).context("Could not migrate config")?;
|
||||
manifest::migrate(tauri_dir).context("Could not migrate manifest")?;
|
||||
frontend::migrate(app_dir, tauri_dir)?;
|
||||
let plugins = frontend::migrate(app_dir)?;
|
||||
|
||||
migrated.plugins.extend(plugins);
|
||||
|
||||
// Add plugins
|
||||
for plugin in migrated.plugins {
|
||||
|
|
Loading…
Reference in New Issue