From 3bec7b1595e28630a22b9fb16540beafd5eb7969 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 12 Aug 2024 01:45:42 -0300 Subject: [PATCH] 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 --- .changes/migrate-vue-svelte.md | 6 + tooling/cli/Cargo.lock | 1 + tooling/cli/Cargo.toml | 1 + tooling/cli/src/helpers/cargo.rs | 33 +- .../cli/src/migrate/migrations/v1/frontend.rs | 512 +++++++++++------- .../v1/frontend/partial_loader/mod.rs | 69 +++ .../v1/frontend/partial_loader/svelte.rs | 93 ++++ .../v1/frontend/partial_loader/vue.rs | 246 +++++++++ tooling/cli/src/migrate/migrations/v1/mod.rs | 6 +- 9 files changed, 739 insertions(+), 228 deletions(-) create mode 100644 .changes/migrate-vue-svelte.md create mode 100644 tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs create mode 100644 tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/svelte.rs create mode 100644 tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs diff --git a/.changes/migrate-vue-svelte.md b/.changes/migrate-vue-svelte.md new file mode 100644 index 000000000..f09cf6e2a --- /dev/null +++ b/.changes/migrate-vue-svelte.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +v1 migrate script now migrates Svelte and Vue.js code. diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index 7b07ee782..1361ee84c 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -5228,6 +5228,7 @@ dependencies = [ "local-ip-address", "log", "magic_string", + "memchr", "minisign", "notify", "notify-debouncer-mini", diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index 76d5eaac4..24fcb10ee 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -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" diff --git a/tooling/cli/src/helpers/cargo.rs b/tooling/cli/src/helpers/cargo.rs index c2cdac7ed..24628bb9d 100644 --- a/tooling/cli/src/helpers/cargo.rs +++ b/tooling/cli/src/helpers/cargo.rs @@ -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::>() - .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"); diff --git a/tooling/cli/src/migrate/migrations/v1/frontend.rs b/tooling/cli/src/migrate/migrations/v1/frontend.rs index b171e7a9d..b68b59eb0 100644 --- a/tooling/cli/src/migrate/migrations/v1/frontend.rs +++ b/tooling/cli/src/migrate/migrations/v1/frontend.rs @@ -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> { 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, - new_npm_packages: &mut Vec, + new_plugins: &mut Vec, ) -> crate::Result { 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#" + + + + + +"#; + + let expected = r#" + + + + + +"#; + + 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#" +
+
+ + +"#; + + let expected = r#" +
+
+ + +"#; + + 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" ] ); } diff --git a/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs b/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs new file mode 100644 index 000000000..bb95b33c0 --- /dev/null +++ b/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs @@ -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 = " { + 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>> { + 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. `" + 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#" + +

Hello World

+ "#; + + 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#" + +

Hello World

+ "#; + + let result = parse_svelte(source_text); + assert_eq!(result.source_text.trim(), r#"console.log("hi");"#); + } +} diff --git a/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs b/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs new file mode 100644 index 000000000..6d5a8ee28 --- /dev/null +++ b/tooling/cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs @@ -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> { + self.parse_scripts() + } + + /// Each *.vue file can contain at most + /// * one `" + 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#" + + + "#; + + 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#" + + "#; + + 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" + + "; + + 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" + + "; + + 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" + + "; + + 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" + + + "; + + 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" + + "; + + 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" + + "; + + let result = parse_vue(source_text); + assert_eq!(result.source_text.trim(), r"`${/{/}`"); + } + + #[test] + fn test_no_script() { + let source_text = r" + + "; + + let sources = VuePartialLoader::new(source_text).parse(); + assert!(sources.is_empty()); + } + + #[test] + fn test_syntax_error() { + let source_text = r" + + + "; + 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" + + "; + + let result = parse_vue(source_text); + assert_eq!( + result.source_text.trim(), + "let 日历 = '2000年'; + const t = useTranslate({ + 'zh-CN': { + calendar: '日历', + tiledDisplay: '平铺展示', + }, + });" + .trim() + ); + } +} diff --git a/tooling/cli/src/migrate/migrations/v1/mod.rs b/tooling/cli/src/migrate/migrations/v1/mod.rs index 46e6f26e3..e378ab35a 100644 --- a/tooling/cli/src/migrate/migrations/v1/mod.rs +++ b/tooling/cli/src/migrate/migrations/v1/mod.rs @@ -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 {