From ae831b111fca2453e3dad6139e3d7653c55177c4 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Wed, 27 Nov 2024 09:07:39 +0100 Subject: [PATCH] WIP --- cli/tools/bundle/bundle_graph.rs | 33 ++- cli/tools/bundle/bundle_resolver.rs | 173 +++++++++++++ cli/tools/bundle/chunk_graph.rs | 191 ++++++++++++++ cli/tools/bundle/mod.rs | 384 ++-------------------------- cli/tools/bundle/transform/mod.rs | 36 +++ 5 files changed, 454 insertions(+), 363 deletions(-) create mode 100644 cli/tools/bundle/bundle_resolver.rs create mode 100644 cli/tools/bundle/chunk_graph.rs create mode 100644 cli/tools/bundle/transform/mod.rs diff --git a/cli/tools/bundle/bundle_graph.rs b/cli/tools/bundle/bundle_graph.rs index dd555aa070..4710c60f56 100644 --- a/cli/tools/bundle/bundle_graph.rs +++ b/cli/tools/bundle/bundle_graph.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use deno_ast::MediaType; +use deno_ast::{MediaType, ParsedSource}; use deno_core::url::Url; use deno_graph::{ExternalModule, JsonModule, WasmModule}; @@ -16,11 +16,12 @@ pub struct BundleJsModule { pub specifier: Url, pub media_type: MediaType, pub source: String, + pub ast: Option, pub dependencies: Vec, } #[derive(Debug)] -pub enum BundleMod { +pub enum BundleModule { Js(BundleJsModule), Json(JsonModule), Wasm(WasmModule), @@ -32,7 +33,7 @@ pub enum BundleMod { pub struct BundleGraph { id: usize, url_to_id: HashMap, - modules: HashMap, + modules: HashMap, } /// The bundle graph only contains fully resolved modules. @@ -45,7 +46,29 @@ impl BundleGraph { } } - pub fn insert(&mut self, specifier: Url, module: BundleMod) -> usize { + pub fn get_specifier(&self, id: usize) -> Option { + if let Some(module) = self.modules.get(&id) { + match module { + BundleModule::Js(m) => Some(m.specifier.clone()), + BundleModule::Json(m) => Some(m.specifier.clone()), + BundleModule::Wasm(m) => Some(m.specifier.clone()), + BundleModule::Node(_) => None, // FIXME + BundleModule::External(external_module) => todo!(), + } + } else { + None + } + } + + pub fn get(&self, url: &Url) -> Option<&BundleModule> { + if let Some(id) = self.url_to_id.get(&url) { + return self.modules.get(&id); + } + + None + } + + pub fn insert(&mut self, specifier: Url, module: BundleModule) -> usize { let id = self.register(specifier); self.modules.insert(id, module); id @@ -63,7 +86,7 @@ impl BundleGraph { } pub fn add_dependency(&mut self, id: usize, dep: BundleDep) { - if let Some(BundleMod::Js(module)) = self.modules.get_mut(&id) { + if let Some(BundleModule::Js(module)) = self.modules.get_mut(&id) { module.dependencies.push(dep) } } diff --git a/cli/tools/bundle/bundle_resolver.rs b/cli/tools/bundle/bundle_resolver.rs new file mode 100644 index 0000000000..9a0fc83848 --- /dev/null +++ b/cli/tools/bundle/bundle_resolver.rs @@ -0,0 +1,173 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use deno_core::{anyhow::Context, error::AnyError, url::Url}; +use deno_graph::{GraphKind, Module, ModuleGraph, NpmModule}; +use deno_runtime::deno_node::NodeResolver; +use indexmap::IndexSet; +use node_resolver::{NodeModuleKind, NodeResolutionMode}; + +use crate::{ + graph_util::{CreateGraphOptions, ModuleGraphCreator}, + npm::CliNpmResolver, + tools::bundle::bundle_graph::BundleDep, +}; + +use super::bundle_graph::{BundleGraph, BundleJsModule, BundleModule}; + +pub async fn build_resolved_graph( + module_graph_creator: &Arc, + npm_resolver: &Arc, + node_resolver: &Arc, + files: Vec, +) -> Result { + let graph = module_graph_creator + .create_graph_with_options(CreateGraphOptions { + graph_kind: GraphKind::CodeOnly, + roots: files.clone(), + is_dynamic: false, + loader: None, + }) + .await?; + + graph.valid()?; + + let mut bundle_graph = BundleGraph::new(); + + let mut npm_modules: IndexSet = IndexSet::new(); + let mut seen: HashSet = HashSet::new(); + let mut pending_npm_dep_links: HashMap = HashMap::new(); + + walk_graph( + &graph, + &mut seen, + npm_resolver, + node_resolver, + &mut bundle_graph, + &mut npm_modules, + &mut pending_npm_dep_links, + ); + + // Resolve npm modules + // Hack: Create sub graphs for every npm module we encounter and + // expand npm specifiers to the actual file + while let Some(url) = npm_modules.pop() { + let npm_graph = module_graph_creator + .create_graph_with_options(CreateGraphOptions { + graph_kind: GraphKind::CodeOnly, + roots: vec![url.clone()], + is_dynamic: false, + loader: None, + }) + .await?; + + walk_graph( + &npm_graph, + &mut seen, + npm_resolver, + node_resolver, + &mut bundle_graph, + &mut npm_modules, + &mut pending_npm_dep_links, + ); + } + + Ok(bundle_graph) +} + +fn walk_graph( + graph: &ModuleGraph, + seen: &mut HashSet, + npm_resolver: &Arc, + node_resolver: &Arc, + bundle_graph: &mut BundleGraph, + npm_modules: &mut IndexSet, + pending_npm_dep_links: &mut HashMap, +) { + for module in graph.modules() { + let url = module.specifier(); + + if seen.contains(&url) { + continue; + } + + seen.insert(url.clone()); + + match module { + Module::Npm(module) => { + let resolved = resolve_npm_module(&module, npm_resolver, node_resolver); + npm_modules.insert(resolved.clone()); + } + Module::Js(js_module) => { + let id = bundle_graph.insert( + url.clone(), + BundleModule::Js(BundleJsModule { + specifier: url.clone(), + media_type: js_module.media_type, + source: js_module.source.to_string(), + ast: None, + dependencies: vec![], + }), + ); + + for (raw, dep) in &js_module.dependencies { + if let Some(code) = dep.get_code() { + if code.scheme() == "npm" { + pending_npm_dep_links.insert(id, code.clone()); + } else { + let dep_id = bundle_graph.register(code.clone()); + bundle_graph.add_dependency( + id, + BundleDep { + id: dep_id, + raw: raw.to_string(), + is_dyanmic: dep.is_dynamic, + }, + ); + } + } + } + } + Module::Json(json_module) => { + bundle_graph + .insert(url.clone(), BundleModule::Json(json_module.clone())); + } + Module::Wasm(wasm_module) => { + bundle_graph + .insert(url.clone(), BundleModule::Wasm(wasm_module.clone())); + } + Module::Node(built_in_node_module) => { + bundle_graph.insert( + url.clone(), + BundleModule::Node(built_in_node_module.module_name.to_string()), + ); + } + Module::External(external_module) => todo!(), + } + } +} + +fn resolve_npm_module( + module: &NpmModule, + npm_resolver: &Arc, + node_resolver: &Arc, +) -> Url { + let nv = module.nv_reference.nv(); + let managed = npm_resolver.as_managed().unwrap(); + let package_folder = managed.resolve_pkg_folder_from_deno_module(nv).unwrap(); + + let resolved = node_resolver + .resolve_package_subpath_from_deno_module( + &package_folder, + module.nv_reference.sub_path(), + None, // FIXME + NodeModuleKind::Esm, // FIXME + NodeResolutionMode::Execution, + ) + .with_context(|| format!("Could not resolve '{}'.", module.nv_reference)) + .unwrap(); + + resolved +} diff --git a/cli/tools/bundle/chunk_graph.rs b/cli/tools/bundle/chunk_graph.rs new file mode 100644 index 0000000000..f2e39fdf09 --- /dev/null +++ b/cli/tools/bundle/chunk_graph.rs @@ -0,0 +1,191 @@ +use std::collections::{HashMap, HashSet}; + +use deno_core::url::Url; +use indexmap::IndexSet; + +use crate::args::{BundleFlags, BundlePlatform}; + +use super::bundle_graph::{BundleGraph, BundleModule}; + +pub fn assign_chunks( + bundle_flags: &BundleFlags, + chunk_graph: &mut ChunkGraph, + graph: &BundleGraph, + url: &Url, + parent_chunk_id: Option, + is_dynamic: bool, +) { + let module = graph.get(url).unwrap(); + + match &module { + &BundleModule::Js(js_module) => { + let chunk_id = chunk_graph.assign_specifier_to_chunk( + url, + parent_chunk_id, + ChunkKind::Js, + is_dynamic, + ); + + for dep in &js_module.dependencies { + if let Some(specifier) = graph.get_specifier(dep.id) { + assign_chunks( + bundle_flags, + chunk_graph, + graph, + &specifier, + Some(chunk_id), + dep.is_dyanmic, + ); + } + } + } + BundleModule::Json(json_module) => { + chunk_graph.assign_specifier_to_chunk( + url, + parent_chunk_id, + ChunkKind::Js, + is_dynamic, + ); + } + BundleModule::Wasm(wasm_module) => { + chunk_graph.assign_specifier_to_chunk( + url, + parent_chunk_id, + ChunkKind::Asset("wasm".to_string()), + true, + ); + } + BundleModule::Node(built_in_node_module) => { + if let BundlePlatform::Browser = bundle_flags.platform { + // TODO: Show where it was imported from + log::log!( + log::Level::Error, + "Imported Node internal module '{}' which will fail in browsers.", + built_in_node_module + ); + } + } + BundleModule::External(external_module) => todo!(), + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum ChunkKind { + Asset(String), + Js, +} + +#[derive(Debug)] +pub struct Chunk { + pub id: usize, + pub name: String, + pub kind: ChunkKind, + pub parent_ids: HashSet, + pub children: Vec, // TODO: IndexSet? + pub specifiers: IndexSet, +} + +#[derive(Debug)] +pub struct ChunkGraph { + pub id: usize, + pub chunks: HashMap, + pub root_chunks: HashSet, + pub specifier_to_chunks: HashMap>, +} + +impl ChunkGraph { + pub fn new() -> Self { + Self { + id: 0, + chunks: HashMap::new(), + root_chunks: HashSet::new(), + specifier_to_chunks: HashMap::new(), + } + } + + fn get_or_create_chunk( + &mut self, + url: &Url, + parent_chunk_id: Option, + kind: ChunkKind, + is_dynamic: bool, + ) -> usize { + if let Some(parent_chunk_id) = parent_chunk_id { + if !is_dynamic { + return parent_chunk_id; + } + } + + let name = if let Ok(f) = url.to_file_path() { + if let Some(name) = f.file_stem() { + name.to_string_lossy().to_string() + } else { + format!("chunk_{}", self.id) + } + } else { + format!("chunk_{}", self.id) + }; + + let ext = match &kind { + ChunkKind::Asset(ext) => ext, + ChunkKind::Js => "js", + }; + + let full_name = format!("{}.{}", name, ext); + + self.new_chunk(full_name, parent_chunk_id, kind) + } + + pub fn assign_specifier_to_chunk( + &mut self, + url: &Url, + parent_chunk_id: Option, + kind: ChunkKind, + is_dynamic: bool, + ) -> usize { + let chunk_id = + self.get_or_create_chunk(&url, parent_chunk_id, kind, is_dynamic); + + if let Some(value) = self.specifier_to_chunks.get_mut(&url) { + value.push(chunk_id) + } else { + let value = vec![chunk_id]; + self.specifier_to_chunks.insert(url.clone(), value); + } + + if let Some(chunk) = self.chunks.get_mut(&chunk_id) { + chunk.specifiers.insert(url.clone()); + } + + chunk_id + } + + fn new_chunk( + &mut self, + name: String, + parent_id: Option, + kind: ChunkKind, + ) -> usize { + let id = self.id; + self.id += 1; + + let mut parent_ids = HashSet::new(); + if let Some(parent_id) = parent_id { + parent_ids.insert(parent_id); + } else { + self.root_chunks.insert(id); + } + + let chunk = Chunk { + id, + name, + kind, + parent_ids, + children: vec![], + specifiers: IndexSet::new(), + }; + + self.chunks.insert(id, chunk); + id + } +} diff --git a/cli/tools/bundle/mod.rs b/cli/tools/bundle/mod.rs index 73b098e8c4..2bcfa48fd2 100644 --- a/cli/tools/bundle/mod.rs +++ b/cli/tools/bundle/mod.rs @@ -1,31 +1,28 @@ use std::{ cmp::Ordering, - collections::{HashMap, HashSet}, fs, io::Write, path::{Path, PathBuf}, sync::Arc, }; -use bundle_graph::{BundleDep, BundleGraph, BundleJsModule, BundleMod}; -use deno_core::{anyhow::Context, error::AnyError, url::Url}; -use deno_graph::{GraphKind, Module, ModuleGraph, NpmModule, Resolution}; -use deno_runtime::{colors, deno_node::NodeResolver}; -use deno_semver::package; +use bundle_graph::BundleModule; +use bundle_resolver::build_resolved_graph; +use chunk_graph::{assign_chunks, ChunkGraph}; +use deno_core::error::AnyError; +use deno_runtime::colors; use flate2::{write::ZlibEncoder, Compression}; -use indexmap::IndexSet; -use node_resolver::{NodeModuleKind, NodeResolutionMode}; use crate::{ - args::{BundleFlags, BundlePlatform, Flags}, + args::{BundleFlags, Flags}, factory::CliFactory, - graph_util::CreateGraphOptions, - npm::CliNpmResolver, - resolver::CjsTracker, util::{fs::collect_specifiers, path::matches_pattern_or_exact_path}, }; mod bundle_graph; +mod bundle_resolver; +mod chunk_graph; +mod transform; #[derive(Debug)] struct BundleChunkStat { @@ -62,159 +59,20 @@ pub async fn bundle( let module_graph_creator = factory.module_graph_creator().await?; - let graph = module_graph_creator - .create_graph_with_options(CreateGraphOptions { - graph_kind: GraphKind::CodeOnly, - roots: files.clone(), - is_dynamic: false, - loader: None, - }) - .await?; - - graph.valid()?; - - let mut bundle_graph = BundleGraph::new(); - - let mut id = 0; - let mut all_modules: HashMap = HashMap::new(); - let mut module_to_id: HashMap = HashMap::new(); - let mut id_to_module: HashMap = HashMap::new(); - - let mut npm_modules: IndexSet = IndexSet::new(); - let mut seen: IndexSet = IndexSet::new(); - - fn resolve_npm_module( - module: &NpmModule, - npm_resolver: &Arc, - node_resolver: &Arc, - ) -> Url { - let nv = module.nv_reference.nv(); - let managed = npm_resolver.as_managed().unwrap(); - let package_folder = - managed.resolve_pkg_folder_from_deno_module(nv).unwrap(); - - let resolved = node_resolver - .resolve_package_subpath_from_deno_module( - &package_folder, - module.nv_reference.sub_path(), - None, // FIXME - NodeModuleKind::Esm, // FIXME - NodeResolutionMode::Execution, - ) - .with_context(|| format!("Could not resolve '{}'.", module.nv_reference)) - .unwrap(); - - resolved - } - - let mut pending_npm_dep_links: HashMap = HashMap::new(); - - // Hack: Create sub graphs for every npm module we encounter and - // expand npm specifiers to the actual file - for module in graph.modules() { - let url = module.specifier(); - seen.insert(url.clone()); - - let current_id = id; - module_to_id.insert(url.clone(), current_id); - id_to_module.insert(current_id, url.clone()); - - id += 1; - - match module { - Module::Npm(module) => { - let resolved = resolve_npm_module(&module, npm_resolver, node_resolver); - npm_modules.insert(resolved.clone()); - } - Module::Js(js_module) => { - let id = bundle_graph.insert( - url.clone(), - BundleMod::Js(BundleJsModule { - specifier: url.clone(), - media_type: js_module.media_type, - source: js_module.source.to_string(), - dependencies: vec![], - }), - ); - - for (raw, dep) in &js_module.dependencies { - if let Some(code) = dep.get_code() { - if code.scheme() == "npm" { - pending_npm_dep_links.insert(id, code.clone()); - } else { - let dep_id = bundle_graph.register(code.clone()); - bundle_graph.add_dependency( - id, - BundleDep { - id: dep_id, - raw: raw.to_string(), - is_dyanmic: dep.is_dynamic, - }, - ); - } - } - eprintln!("JS dep {} {:#?}", raw, dep); - } - } - Module::Json(json_module) => { - bundle_graph.insert(url.clone(), BundleMod::Json(json_module.clone())); - } - Module::Wasm(wasm_module) => { - bundle_graph.insert(url.clone(), BundleMod::Wasm(wasm_module.clone())); - } - Module::Node(built_in_node_module) => { - bundle_graph.insert( - url.clone(), - BundleMod::Node(built_in_node_module.module_name.to_string()), - ); - } - Module::External(external_module) => todo!(), - } - } - - let npm_modules_vec = - npm_modules.iter().map(|u| u.clone()).collect::>(); - - eprintln!("npm vec: {:#?}", npm_modules_vec); - while let Some(url) = npm_modules.pop() { - let npm_graph = module_graph_creator - .create_graph_with_options(CreateGraphOptions { - graph_kind: GraphKind::CodeOnly, - roots: vec![url.clone()], - is_dynamic: false, - loader: None, - }) - .await?; - - for module in npm_graph.modules() { - if seen.contains(module.specifier()) { - continue; - } - - match module { - Module::Npm(module) => { - let resolved = - resolve_npm_module(&module, npm_resolver, node_resolver); - npm_modules.insert(resolved.clone()); - } - _ => { - all_modules.insert(url.clone(), module.clone()); - } - } - } - - eprintln!("RES {:#?}", all_modules); - } + let bundle_graph = build_resolved_graph( + module_graph_creator, + npm_resolver, + node_resolver, + files.clone(), + ) + .await?; let mut chunk_graph = ChunkGraph::new(); for file in files { assign_chunks( &bundle_flags, &mut chunk_graph, - &all_modules, - npm_resolver, - node_resolver, - cjs_tracker, + &bundle_graph, &file, None, true, @@ -238,11 +96,17 @@ pub async fn bundle( let mut source = String::new(); for spec in chunk.specifiers.iter().rev() { - if let Some(module) = graph.get(&spec) { + if let Some(module) = bundle_graph.get(&spec) { // FIXME: don't print module urls by default source.push_str(&format!("// {}\n", spec.to_string())); - if let Some(contents) = &module.source() { - source.push_str(contents); + match module { + BundleModule::Js(bundle_js_module) => { + source.push_str(&bundle_js_module.source); + } + BundleModule::Json(json_module) => todo!(), + BundleModule::Wasm(wasm_module) => todo!(), + BundleModule::Node(_) => todo!(), + BundleModule::External(external_module) => todo!(), } } } @@ -302,199 +166,3 @@ pub async fn bundle( Ok(()) } - -fn assign_chunks( - bundle_flags: &BundleFlags, - chunk_graph: &mut ChunkGraph, - graph: &HashMap, - npm_resolver: &Arc, - node_resolver: &Arc, - cjs_tracker: &Arc, - url: &Url, - parent_chunk_id: Option, - is_dynamic: bool, -) { - let module = graph.get(url).unwrap(); - - match module { - Module::Js(js_module) => { - let chunk_id = chunk_graph.assign_specifier_to_chunk( - url, - parent_chunk_id, - ChunkKind::Js, - is_dynamic, - ); - - for (_, dep) in &js_module.dependencies { - match &dep.maybe_code { - Resolution::None => todo!(), - Resolution::Ok(resolution_resolved) => { - assign_chunks( - bundle_flags, - chunk_graph, - graph, - npm_resolver, - node_resolver, - cjs_tracker, - &resolution_resolved.specifier, - Some(chunk_id), - dep.is_dynamic, - ); - } - Resolution::Err(resolution_error) => todo!(), - } - } - } - Module::Json(json_module) => { - chunk_graph.assign_specifier_to_chunk( - url, - parent_chunk_id, - ChunkKind::Js, - is_dynamic, - ); - } - Module::Wasm(wasm_module) => { - chunk_graph.assign_specifier_to_chunk( - url, - parent_chunk_id, - ChunkKind::Asset("wasm".to_string()), - true, - ); - } - Module::Npm(_) => { - unreachable!() - } - Module::Node(built_in_node_module) => { - if let BundlePlatform::Browser = bundle_flags.platform { - // TODO: Show where it was imported from - log::log!( - log::Level::Error, - "Imported Node internal module '{}' which will fail in browsers.", - built_in_node_module.specifier.to_string() - ); - } - } - Module::External(external_module) => todo!(), - } -} - -#[derive(Debug, Eq, PartialEq)] -enum ChunkKind { - Asset(String), - Js, -} - -#[derive(Debug)] -struct Chunk { - id: usize, - name: String, - pub kind: ChunkKind, - parent_ids: HashSet, - children: Vec, // TODO: IndexSet? - specifiers: IndexSet, -} - -#[derive(Debug)] -struct ChunkGraph { - pub id: usize, - pub chunks: HashMap, - pub root_chunks: HashSet, - pub specifier_to_chunks: HashMap>, -} - -impl ChunkGraph { - fn new() -> Self { - Self { - id: 0, - chunks: HashMap::new(), - root_chunks: HashSet::new(), - specifier_to_chunks: HashMap::new(), - } - } - - fn get_or_create_chunk( - &mut self, - url: &Url, - parent_chunk_id: Option, - kind: ChunkKind, - is_dynamic: bool, - ) -> usize { - if let Some(parent_chunk_id) = parent_chunk_id { - if !is_dynamic { - return parent_chunk_id; - } - } - - let name = if let Ok(f) = url.to_file_path() { - if let Some(name) = f.file_stem() { - name.to_string_lossy().to_string() - } else { - format!("chunk_{}", self.id) - } - } else { - format!("chunk_{}", self.id) - }; - - let ext = match &kind { - ChunkKind::Asset(ext) => ext, - ChunkKind::Js => "js", - }; - - let full_name = format!("{}.{}", name, ext); - - self.new_chunk(full_name, parent_chunk_id, kind) - } - - fn assign_specifier_to_chunk( - &mut self, - url: &Url, - parent_chunk_id: Option, - kind: ChunkKind, - is_dynamic: bool, - ) -> usize { - let chunk_id = - self.get_or_create_chunk(&url, parent_chunk_id, kind, is_dynamic); - - if let Some(value) = self.specifier_to_chunks.get_mut(&url) { - value.push(chunk_id) - } else { - let value = vec![chunk_id]; - self.specifier_to_chunks.insert(url.clone(), value); - } - - if let Some(chunk) = self.chunks.get_mut(&chunk_id) { - chunk.specifiers.insert(url.clone()); - } - - chunk_id - } - - fn new_chunk( - &mut self, - name: String, - parent_id: Option, - kind: ChunkKind, - ) -> usize { - let id = self.id; - self.id += 1; - - let mut parent_ids = HashSet::new(); - if let Some(parent_id) = parent_id { - parent_ids.insert(parent_id); - } else { - self.root_chunks.insert(id); - } - - let chunk = Chunk { - id, - name, - kind, - parent_ids, - children: vec![], - specifiers: IndexSet::new(), - }; - - self.chunks.insert(id, chunk); - id - } -} diff --git a/cli/tools/bundle/transform/mod.rs b/cli/tools/bundle/transform/mod.rs new file mode 100644 index 0000000000..3db308cd36 --- /dev/null +++ b/cli/tools/bundle/transform/mod.rs @@ -0,0 +1,36 @@ +use deno_ast::{ + parse_module, swc::visit::FoldWith, ParseParams, ParsedSource, SourceMap, +}; + +use super::bundle_graph::{BundleJsModule, BundleModule}; + +pub fn transform_bundle(module: &BundleModule) { + // + + match module { + BundleModule::Js(module) => { + let parsed_source = parse_module(ParseParams { + specifier: module.specifier.clone(), + media_type: module.media_type, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: false, + text: module.source.clone().into(), + }) + .unwrap(); + + // TODO: optional + let source_map = + SourceMap::single(module.specifier.clone(), module.source.clone()); + + let program = parsed_source.program(); + + // Transpile + } + + // FIXME + BundleModule::Json(_json_module) => todo!(), + BundleModule::Wasm(_wasm_module) => {} + BundleModule::Node(_) | BundleModule::External(_) => {} + } +}