Original commit: b16ac40a3d
This commit is contained in:
Wojciech Daniło 2020-12-24 05:38:01 +01:00 committed by GitHub
parent 6a7bee1776
commit 89d990c5b3
12 changed files with 269 additions and 161 deletions

View File

@ -72,17 +72,18 @@ function command(docs) {
return {docs} return {docs}
} }
function run_project_manager() { // FIXME: this does not work if project manager was not downloaded yet.
const bin_path = paths.get_project_manager_path(paths.dist.bin) //function run_project_manager() {
console.log(`Starting the language server from "${bin_path}".`) // const bin_path = paths.get_project_manager_path(paths.dist.bin)
child_process.execFile(bin_path, [], (error, stdout, stderr) => { // console.log(`Starting the language server from "${bin_path}".`)
console.error(stderr) // child_process.execFile(bin_path, [], (error, stdout, stderr) => {
if (error) { // console.error(stderr)
throw error // if (error) {
} // throw error
console.log(stdout) // }
}) // console.log(stdout)
} // })
//}
// ================ // ================
// === Commands === // === Commands ===
@ -241,7 +242,8 @@ commands.watch.rust = async function(argv) {
build_args.push(`--crate=${argv.crate}`) build_args.push(`--crate=${argv.crate}`)
} }
run_project_manager() // FIXME: See fixme on fn definition.
// run_project_manager()
build_args = build_args.join(' ') build_args = build_args.join(' ')
let target = let target =
'"' + '"' +

View File

@ -147,9 +147,9 @@ are presented below:
utility which will build the project on every change. Open utility which will build the project on every change. Open
`http://localhost:8080` (the port may vary and will be reported in the `http://localhost:8080` (the port may vary and will be reported in the
terminal if `8080` was already in use) to run the application, or terminal if `8080` was already in use) to run the application, or
`http://localhost:8080/debug` to open example demo scenes. Please remember `http://localhost:8080/?entry` to open example demo scenes list. Please
to disable the cache in your browser during the development! By default, remember to disable the cache in your browser during the development! By
the script disables heavyweight optimizations to provide interactive default, the script disables heavyweight optimizations to provide interactive
development experience. The scripts are thin wrappers for development experience. The scripts are thin wrappers for
[wasm-pack](https://github.com/rustwasm/wasm-pack) and accept the same [wasm-pack](https://github.com/rustwasm/wasm-pack) and accept the same
[command line arguments](https://rustwasm.github.io/wasm-pack/book/commands/build.html). [command line arguments](https://rustwasm.github.io/wasm-pack/book/commands/build.html).
@ -158,8 +158,8 @@ are presented below:
In order to compile in a production mode (enable all optimizations, strip In order to compile in a production mode (enable all optimizations, strip
WASM debug symbols, minimize the output binaries, etc.), run WASM debug symbols, minimize the output binaries, etc.), run
`node ./run build`. To create platform-specific packages and installers use `node ./run build`. To create platform-specific packages and installers use
`node ./run dist` instead. The final packages will be located at `node ./run dist` instead. The final executables will be located at
`app/dist/native`. `dist/client/$PLATFORM`.
- **Selective mode** - **Selective mode**
In order to compile only part of the project, and thus drastically shorten In order to compile only part of the project, and thus drastically shorten
@ -173,6 +173,19 @@ are presented below:
were defined or re-exported by that crate. In particular, the `ide` crate were defined or re-exported by that crate. In particular, the `ide` crate
exposes the `entry_point_ide` function, so you have to compile it to test exposes the `entry_point_ide` function, so you have to compile it to test
your code in the Enso IDE. your code in the Enso IDE.
### Using IDE as a library.
In case you want to use the IDE as a library, for example to embed it into
another website, you need to first build it using `node ./run {built,dist}` and
find the necessary artifacts located at `dist/content`. Especially, the
`dist/content/index.js` defines a function `window.enso.main(cfg)` which you
can use to run the IDE. Currently, the configuration argument can contain the
following options:
- `entry` - the entry point, one of predefined scenes. Set it to empty string to
see the list of possible entry points.
- `project` - the project name to open after loading the IDE.
### Testing, Linting, and Validation ### Testing, Linting, and Validation
After changing the code it's always a good idea to lint and test the code. We After changing the code it's always a good idea to lint and test the code. We

8
gui/src/config.yaml Normal file
View File

@ -0,0 +1,8 @@
# The name of an object created in the global window scope. This object will contain functions
# to control the IDE, like the `main` function, and also the configuration object.
windowAppScopeName: "enso"
# The configuration object nested inside of the `windowAppScopeName` object, containing all
# startup configuration options. See usages of this variable to learn more about available
# options.
windowAppScopeConfigName: "config"

View File

@ -10,7 +10,8 @@ let config = {
}, },
devDependencies: { devDependencies: {
"compression-webpack-plugin": "^3.1.0", "compression-webpack-plugin": "^3.1.0",
"copy-webpack-plugin": "^5.1.1" "copy-webpack-plugin": "^5.1.1",
"yaml-loader": "^0.6.0",
} }
} }

View File

@ -69,9 +69,9 @@
} }
</style> </style>
<script type="module" src="/assets/index.js" defer></script> <script type="module" src="/assets/index.js" defer></script>
<script type="module" src="/assets/run.js" defer></script>
</head> </head>
<body> <body>
<!-- <div class="titlebar"></div> -->
<div id="root"></div> <div id="root"></div>
<noscript> <noscript>
This page requires JavaScript to run. Please enable it in your browser. This page requires JavaScript to run. Please enable it in your browser.

View File

@ -5,6 +5,16 @@
import * as loader_module from 'enso-studio-common/src/loader' import * as loader_module from 'enso-studio-common/src/loader'
import * as html_utils from 'enso-studio-common/src/html_utils' import * as html_utils from 'enso-studio-common/src/html_utils'
import * as animation from 'enso-studio-common/src/animation' import * as animation from 'enso-studio-common/src/animation'
import * as globalConfig from '../../../../config.yaml'
// ==================
// === Global API ===
// ==================
let API = {}
window[globalConfig.windowAppScopeName] = API
@ -109,18 +119,13 @@ function show_debug_screen(wasm,msg) {
ul.appendChild(li) ul.appendChild(li)
a.appendChild(linkText) a.appendChild(linkText)
a.title = name a.title = name
a.href = "javascript:{}" a.href = "?entry="+name
a.onclick = () => {
html_utils.remove_node(debug_screen_div)
let fn_name = wasm_entry_point_pfx + name
let fn = wasm[fn_name]
fn()
}
li.appendChild(a) li.appendChild(a)
} }
} }
// ==================== // ====================
// === Scam Warning === // === Scam Warning ===
// ==================== // ====================
@ -191,7 +196,7 @@ window.logsBuffer = logsBuffer
let root = document.getElementById('root') let root = document.getElementById('root')
function prepare_root(cfg) { function style_root() {
root.style.backgroundColor = '#f6f3f199' root.style.backgroundColor = '#f6f3f199'
} }
@ -218,43 +223,36 @@ function disableContextMenu() {
}) })
} }
function ok(value) {
return value !== null && value !== undefined
}
/// Main entry point. Loads WASM, initializes it, chooses the scene to run. /// Main entry point. Loads WASM, initializes it, chooses the scene to run.
async function main() { API.main = async function (inputConfig) {
let urlParams = new URLSearchParams(window.location.search);
let urlConfig = Object.fromEntries(urlParams.entries())
let config = Object.assign({},inputConfig,urlConfig)
API[globalConfig.windowAppScopeConfigName] = config
style_root()
printScamWarning() printScamWarning()
hideLogs() hideLogs()
disableContextMenu() disableContextMenu()
let location = window.location.pathname.split('/')
location.splice(0,1)
let cfg = getUrlParams()
prepare_root(cfg)
let debug_mode = location[0] == "debug" let entryTarget = ok(config.entry) ? config.entry : main_entry_point
let debug_target = location[1] let useLoader = entryTarget === main_entry_point
let no_loader = debug_mode && debug_target
await windowShowAnimation() await windowShowAnimation()
let {wasm,loader} = await download_content({no_loader}) let {wasm,loader} = await download_content({no_loader:!useLoader})
let target = null; if (entryTarget) {
if (debug_mode) { let fn_name = wasm_entry_point_pfx + entryTarget
loader.destroy()
if (debug_target) {
target = debug_target
}
} else {
target = main_entry_point
}
if (target) {
let fn_name = wasm_entry_point_pfx + target
let fn = wasm[fn_name] let fn = wasm[fn_name]
if (fn) { fn() } else { if (fn) { fn() } else {
loader.destroy() loader.destroy()
show_debug_screen(wasm,"Unknown entry point '" + target + "'. ") show_debug_screen(wasm,"Unknown entry point '" + entryTarget + "'. ")
} }
} else { } else {
show_debug_screen(wasm) show_debug_screen(wasm)
} }
} }
main()

View File

@ -0,0 +1,4 @@
/// This file is used to simply run the IDE. It can be not invoked if the IDE needs to be used as a
/// library.
window.enso.main()

View File

@ -24,6 +24,7 @@ module.exports = {
new CompressionPlugin(), new CompressionPlugin(),
new CopyWebpackPlugin([ new CopyWebpackPlugin([
path.resolve(thisPath,'src','index.html'), path.resolve(thisPath,'src','index.html'),
path.resolve(thisPath,'src','run.js'),
path.resolve(wasmPath,'ide.wasm'), path.resolve(wasmPath,'ide.wasm'),
]), ]),
], ],
@ -42,5 +43,14 @@ module.exports = {
hints: false, hints: false,
}, },
mode: 'none', mode: 'none',
stats: 'minimal' stats: 'minimal',
module: {
rules: [
{
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader'
}
]
}
} }

View File

@ -1212,9 +1212,9 @@
} }
}, },
"@octokit/openapi-types": { "@octokit/openapi-types": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz",
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==", "integrity": "sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q==",
"dev": true "dev": true
}, },
"@octokit/plugin-enterprise-rest": { "@octokit/plugin-enterprise-rest": {
@ -1358,12 +1358,12 @@
} }
}, },
"@octokit/types": { "@octokit/types": {
"version": "6.1.1", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.2.tgz",
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==", "integrity": "sha512-LPCpcLbcky7fWfHCTuc7tMiSHFpFlrThJqVdaHgowBTMS0ijlZFfonQC/C1PrZOjD4xRCYgBqH9yttEATGE/nw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@octokit/openapi-types": "^2.0.0", "@octokit/openapi-types": "^2.0.1",
"@types/node": ">= 8" "@types/node": ">= 8"
} }
}, },
@ -13038,6 +13038,20 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}, },
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg=="
},
"yaml-loader": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.6.0.tgz",
"integrity": "sha512-1bNiLelumURyj+zvVHOv8Y3dpCri0F2S+DCcmps0pA1zWRLjS+FhZQg4o3aUUDYESh73+pKZNI18bj7stpReow==",
"requires": {
"loader-utils": "^1.4.0",
"yaml": "^1.8.3"
}
},
"yargs": { "yargs": {
"version": "15.4.1", "version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",

View File

@ -100,7 +100,7 @@ impl Display for IpWithSocket {
} }
/// Project name. /// Project name.
#[derive(Debug,Display,Clone,Serialize,Deserialize,PartialEq,Shrinkwrap)] #[derive(Debug,Display,Clone,Serialize,Deserialize,From,PartialEq,Shrinkwrap)]
#[shrinkwrap(mutable)] #[shrinkwrap(mutable)]
pub struct ProjectName(pub String); pub struct ProjectName(pub String);

View File

@ -8,26 +8,14 @@ use ensogl::system::web;
// =================
// === Constants ===
// =================
mod connection_arguments {
pub const PROJECT_MANAGER : &str = "project_manager";
pub const LANGUAGE_SERVER_JSON : &str = "language_server_rpc";
pub const LANGUAGE_SERVER_BINARY : &str = "language_server_data";
}
// ============== // ==============
// === Errors === // === Errors ===
// ============== // ==============
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Fail)] #[derive(Clone,Copy,Debug,Fail)]
#[fail(display="Missing program option: {}.",name)] #[fail(display="Missing program option: {}.",0)]
pub struct MissingOption {name:&'static str} pub struct MissingOption (&'static str);
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Fail)] #[derive(Clone,Copy,Debug,Fail)]
@ -36,9 +24,9 @@ pub struct MutuallyExclusiveOptions;
// ================== // ======================
// === Connection === // === BackendService ===
// ================== // ======================
/// A Configuration defining to what backend service should IDE connect. /// A Configuration defining to what backend service should IDE connect.
#[allow(missing_docs)] #[allow(missing_docs)]
@ -64,36 +52,127 @@ impl Default for BackendService {
impl BackendService { impl BackendService {
/// Read backend configuration from the web arguments. See also [`web::Arguments`] /// Read backend configuration from the web arguments. See also [`web::Arguments`]
/// documentation. /// documentation.
pub fn from_web_arguments(arguments:&web::Arguments) -> FallibleResult<Self> { pub fn from_web_arguments(config:&ConfigReader) -> FallibleResult<Self> {
let pm_endpoint = arguments.get(connection_arguments::PROJECT_MANAGER).cloned(); if let Some(endpoint) = &config.project_manager {
let ls_json_endpoint = arguments.get(connection_arguments::LANGUAGE_SERVER_JSON).cloned(); if config.language_server_rpc.is_some() || config.language_server_data.is_some() {
let ls_bin_endpoint = arguments.get(connection_arguments::LANGUAGE_SERVER_BINARY).cloned();
if let Some(endpoint) = pm_endpoint {
if ls_json_endpoint.is_some() || ls_bin_endpoint.is_some() {
Err(MutuallyExclusiveOptions.into()) Err(MutuallyExclusiveOptions.into())
} else { } else {
let endpoint = endpoint.clone();
Ok(Self::ProjectManager {endpoint}) Ok(Self::ProjectManager {endpoint})
} }
} else { } else {
match (ls_json_endpoint,ls_bin_endpoint) { match (&config.language_server_rpc,&config.language_server_data) {
(Some(json_endpoint),Some(binary_endpoint)) => (Some(json_endpoint),Some(binary_endpoint)) => {
Ok(Self::LanguageServer {json_endpoint,binary_endpoint}), let json_endpoint = json_endpoint.clone();
(None,None) => let binary_endpoint = binary_endpoint.clone();
Ok(default()), Ok(Self::LanguageServer {json_endpoint,binary_endpoint})
(Some(_),None) => }
Err(MissingOption{name:connection_arguments::LANGUAGE_SERVER_BINARY}.into()), (None,None) => Ok(default()),
(None,Some(_)) => (Some(_),None) => Err(MissingOption(config.names().language_server_data).into()),
Err(MissingOption{name:connection_arguments::LANGUAGE_SERVER_JSON }.into()) (None,Some(_)) => Err(MissingOption(config.names().language_server_rpc).into())
} }
} }
} }
} }
// ==============
// === Config ===
// ==============
/// The path at which the config is accessible. This needs to be synchronised with the
/// `src/config.yaml` configuration file. In the future, we could write a procedural macro, which
/// loads the configuration and splits Rust variables from it during compilation time. This is not
/// possible by using macro rules, as there is no way to plug in the output of `include_str!` macro
/// to another macro input.
const WINDOW_CFG_PATH : &[&str] = &["enso","config"];
/// Defines a new config structure. The provided fields are converted to optional fields. The config
/// constructor queries JavaScript configuration for the keys defined in this structure. For each
/// resulting string value, it converts it to the defined type. It also reports warnings for all
/// config options that were provided, but were not matched this definition.
macro_rules! define_config {
($name:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
/// Reflection mechanism containing string representation of option names.
#[derive(Clone,Copy,Debug)]
pub struct Names {
$($field : &'static str),*
}
impl Default for Names {
fn default() -> Self {
$(let $field = stringify!{$field};)*
Self {$($field),*}
}
}
/// The structure containing application configs.
#[derive(Clone,Debug,Default)]
pub struct $name {
__names__ : Names,
$($field : Option<$field_type>),*
}
impl $name {
/// Constructor.
pub fn new() -> Self {
let logger = Logger::new(stringify!{$name});
let window = web::window();
match web::reflect_get_nested_object(&window,WINDOW_CFG_PATH).ok() {
None => {
let path = WINDOW_CFG_PATH.join(".");
error!(&logger,"The config path '{path}' is invalid.");
default()
}
Some(cfg) => {
let __names__ = default();
let keys = web::object_keys(&cfg);
let mut keys = keys.into_iter().collect::<HashSet<String>>();
$(
let name = stringify!{$field};
let $field = web::reflect_get_nested_string(&cfg,&[name]).ok();
let $field = $field.map(|t|t.into());
keys.remove(name);
)*
for key in keys {
warning!(&logger,"Unknown config option provided '{key}'.");
}
Self {__names__,$($field),*}
}
}
}
/// Reflection mechanism to get string representation of option names.
pub fn names(&self) -> &Names {
&self.__names__
}
}
};
}
define_config! {
ConfigReader {
entry : String,
project : ProjectName,
project_manager : String,
language_server_rpc : String,
language_server_data : String,
}
}
// ===============
// === Startup ===
// ===============
/// Configuration data necessary to initialize IDE. /// Configuration data necessary to initialize IDE.
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct Startup { pub struct Startup {
/// The configuration of connection to the backend service. /// The configuration of connection to the backend service.
pub backend:BackendService, pub backend : BackendService,
/// The project name we want to open on startup. /// The project name we want to open on startup.
pub project_name : ProjectName pub project_name : ProjectName
} }
@ -110,12 +189,11 @@ impl Default for Startup {
impl Startup { impl Startup {
/// Read configuration from the web arguments. See also [`web::Arguments`] documentation. /// Read configuration from the web arguments. See also [`web::Arguments`] documentation.
pub fn from_web_arguments() -> FallibleResult<Startup> { pub fn from_web_arguments() -> FallibleResult<Startup> {
let arguments = ensogl::system::web::Arguments::new(); let config = ConfigReader::new();
let backend = BackendService::from_web_arguments(&arguments)?; let backend = BackendService::from_web_arguments(&config)?;
let project_name = arguments.get("project").map(ProjectName::new); let project_name = config.project.unwrap_or_else(||
let project_name = project_name.unwrap_or_else(|| {
ProjectName::new(constants::DEFAULT_PROJECT_NAME) ProjectName::new(constants::DEFAULT_PROJECT_NAME)
}); );
Ok(Startup{backend,project_name}) Ok(Startup{backend,project_name})
} }
} }

View File

@ -40,7 +40,7 @@ pub use web_sys::WebGl2RenderingContext;
pub use web_sys::Window; pub use web_sys::Window;
pub use std::time::Duration; pub use std::time::Duration;
pub use std::time::Instant; pub use std::time::Instant;
use std::collections::HashMap;
// ============= // =============
@ -62,6 +62,13 @@ pub fn Error<S:Into<String>>(message:S) -> Error {
pub type Result<T> = std::result::Result<T,Error>; pub type Result<T> = std::result::Result<T,Error>;
impl From<JsValue> for Error {
fn from(t:JsValue) -> Self {
let message = format!("{:?}",t);
Self {message}
}
}
// ============== // ==============
@ -595,50 +602,46 @@ pub async fn sleep(duration:Duration) {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub use async_std::task::sleep; pub use async_std::task::sleep;
/// Stores arguments extracted from `Location`'s search /// Get the nested value of the provided object. This is similar to writing `foo.bar.baz` in
/// (https://developer.mozilla.org/en-US/docs/Web/API/Location/search). /// JavaScript, but in a safe manner, while checking if the value exists on each level.
/// e.g. extracts arg0 = value0, and arg1 = value1 from http://localhost/?arg0=value0&arg1=value1. pub fn reflect_get_nested(target:&JsValue, keys:&[&str]) -> Result<JsValue> {
#[derive(Debug)] let mut tgt = target.clone();
pub struct Arguments { for key in keys {
pub hash_map : HashMap<String,String> let obj = tgt.dyn_into::<js_sys::Object>()?;
} let key = (*key).into();
tgt = js_sys::Reflect::get(&obj,&key)?;
impl Deref for Arguments {
type Target = HashMap<String,String>;
fn deref(&self) -> &Self::Target { &self.hash_map }
}
impl Arguments {
fn args_from_search(search:&str) -> HashMap<String,String> {
if search.chars().nth(0) == Some('?') {
let search_without_question_mark = &search[1..];
search_without_question_mark.split('&').filter_map(|arg| {
match arg.split('=').collect_vec().as_slice() {
[key,value] => Some(((*key).to_string(),(*value).to_string())),
[key] => Some(((*key).to_string(),"".to_string())),
_ => None
}
}).collect()
} else {
default()
}
}
/// Creates a new arguments map from location search.
pub fn new() -> Self {
default()
} }
Ok(tgt)
} }
impl Default for Arguments { /// Get the nested value of the provided object and cast it to [`Object`]. See docs of
fn default() -> Self { /// [`reflect_get_nested`] to learn more.
let search = window().location().search().unwrap_or_default(); pub fn reflect_get_nested_object(target:&JsValue, keys:&[&str]) -> Result<js_sys::Object> {
let hash_map = Self::args_from_search(&search); let tgt = reflect_get_nested(target,keys)?;
Self{hash_map} Ok(tgt.dyn_into()?)
}
} }
/// Get the nested value of the provided object and cast it to [`String`]. See docs of
/// [`reflect_get_nested`] to learn more.
pub fn reflect_get_nested_string(target:&JsValue, keys:&[&str]) -> Result<String> {
let tgt = reflect_get_nested(target,keys)?;
let val = tgt.dyn_into::<js_sys::JsString>()?;
Ok(val.into())
}
/// Get all the keys of the provided [`Object`].
pub fn object_keys(target:&JsValue) -> Vec<String> {
target.clone().dyn_into::<js_sys::Object>().ok().map(|obj| {
js_sys::Object::keys(&obj).iter().map(|key| {
// The unwrap is safe, the `Object::keys` API guarantees it.
let js_str = key.dyn_into::<js_sys::JsString>().unwrap();
js_str.into()
}).collect()
}).unwrap_or_default()
}
// ============ // ============
// === Test === // === Test ===
// ============ // ============
@ -665,29 +668,6 @@ mod tests {
} }
} }
#[test]
fn args() {
let search = String::from("?project=HelloWorld&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 2);
assert_eq!(args.get("project"), Some(&"HelloWorld".to_string()));
assert_eq!(args.get("arg0"), Some(&"value0".to_string()));
let search = String::from("project=HelloWorld&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 0);
let search = String::from("");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 0);
let search = String::from("?project&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 2);
assert_eq!(args.get("project"), Some(&"".to_string()));
assert_eq!(args.get("arg0"), Some(&"value0".to_string()));
}
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod helpers { mod helpers {
use std::time::Instant; use std::time::Instant;