#![allow(
clippy::new_ret_no_self,
clippy::needless_pass_by_value,
clippy::redundant_closure
)]
mod npm;
use std::path::Path;
use std::{collections::HashMap, fs};
use self::npm::{
repository::Repository, CommonJSPackage, ESModulesPackage, NoModulesPackage, NpmPackage,
};
use cargo_metadata::Metadata;
use chrono::offset;
use chrono::DateTime;
use command::build::{BuildProfile, Target};
use curl::easy;
use failure::{Error, ResultExt};
use serde::{self, Deserialize};
use serde_json;
use std::collections::BTreeSet;
use std::env;
use std::io::Write;
use strsim::levenshtein;
use toml;
use PBAR;
const WASM_PACK_METADATA_KEY: &str = "package.metadata.wasm-pack";
const WASM_PACK_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
const WASM_PACK_REPO_URL: &str = "https://github.com/rustwasm/wasm-pack";
pub struct CrateData {
data: Metadata,
current_idx: usize,
manifest: CargoManifest,
out_name: Option<String>,
}
#[doc(hidden)]
#[derive(Deserialize)]
pub struct CargoManifest {
package: CargoPackage,
}
#[derive(Deserialize)]
struct CargoPackage {
name: String,
description: Option<String>,
license: Option<String>,
#[serde(rename = "license-file")]
license_file: Option<String>,
repository: Option<String>,
homepage: Option<String>,
#[serde(default)]
metadata: CargoMetadata,
}
#[derive(Default, Deserialize)]
struct CargoMetadata {
#[serde(default, rename = "wasm-pack")]
wasm_pack: CargoWasmPack,
}
#[derive(Default, Deserialize)]
struct CargoWasmPack {
#[serde(default)]
profile: CargoWasmPackProfiles,
}
#[derive(Deserialize)]
struct CargoWasmPackProfiles {
#[serde(
default = "CargoWasmPackProfile::default_dev",
deserialize_with = "CargoWasmPackProfile::deserialize_dev"
)]
dev: CargoWasmPackProfile,
#[serde(
default = "CargoWasmPackProfile::default_release",
deserialize_with = "CargoWasmPackProfile::deserialize_release"
)]
release: CargoWasmPackProfile,
#[serde(
default = "CargoWasmPackProfile::default_profiling",
deserialize_with = "CargoWasmPackProfile::deserialize_profiling"
)]
profiling: CargoWasmPackProfile,
}
impl Default for CargoWasmPackProfiles {
fn default() -> CargoWasmPackProfiles {
CargoWasmPackProfiles {
dev: CargoWasmPackProfile::default_dev(),
release: CargoWasmPackProfile::default_release(),
profiling: CargoWasmPackProfile::default_profiling(),
}
}
}
#[derive(Default, Deserialize)]
pub struct CargoWasmPackProfile {
#[serde(default, rename = "wasm-bindgen")]
wasm_bindgen: CargoWasmPackProfileWasmBindgen,
#[serde(default, rename = "wasm-opt")]
wasm_opt: Option<CargoWasmPackProfileWasmOpt>,
}
#[derive(Default, Deserialize)]
struct CargoWasmPackProfileWasmBindgen {
#[serde(default, rename = "debug-js-glue")]
debug_js_glue: Option<bool>,
#[serde(default, rename = "demangle-name-section")]
demangle_name_section: Option<bool>,
#[serde(default, rename = "dwarf-debug-info")]
dwarf_debug_info: Option<bool>,
}
struct Collector(Vec<u8>);
impl easy::Handler for Collector {
fn write(&mut self, data: &[u8]) -> Result<usize, easy::WriteError> {
self.0.extend_from_slice(data);
Ok(data.len())
}
}
#[derive(Deserialize, Debug)]
pub struct Crate {
#[serde(rename = "crate")]
crt: CrateInformation,
}
#[derive(Deserialize, Debug)]
struct CrateInformation {
max_version: String,
}
impl Crate {
pub fn return_wasm_pack_latest_version() -> Result<Option<String>, failure::Error> {
let current_time = chrono::offset::Local::now();
let old_metadata_file = Self::return_wasm_pack_file();
match old_metadata_file {
Some(ref file_contents) => {
let last_updated = Self::return_stamp_file_value(&file_contents, "created")
.and_then(|t| DateTime::parse_from_str(t.as_str(), "%+").ok());
last_updated
.map(|last_updated| {
if current_time.signed_duration_since(last_updated).num_hours() > 24 {
Self::return_api_call_result(current_time).map(Some)
} else {
Ok(Self::return_stamp_file_value(&file_contents, "version"))
}
})
.unwrap_or_else(|| Ok(None))
}
None => Self::return_api_call_result(current_time).map(Some),
}
}
fn return_api_call_result(
current_time: DateTime<offset::Local>,
) -> Result<String, failure::Error> {
let version = Self::return_latest_wasm_pack_version();
match version {
Ok(ref version) => Self::override_stamp_file(current_time, Some(&version)).ok(),
Err(_) => Self::override_stamp_file(current_time, None).ok(),
};
version
}
fn override_stamp_file(
current_time: DateTime<offset::Local>,
version: Option<&str>,
) -> Result<(), failure::Error> {
let path = env::current_exe()?;
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.append(true)
.create(true)
.open(path.with_extension("stamp"))?;
file.set_len(0)?;
write!(file, "created {:?}", current_time)?;
if let Some(version) = version {
write!(file, "\nversion {}", version)?;
}
Ok(())
}
fn return_wasm_pack_file() -> Option<String> {
if let Ok(path) = env::current_exe() {
if let Ok(file) = fs::read_to_string(path.with_extension("stamp")) {
return Some(file);
}
}
None
}
fn return_latest_wasm_pack_version() -> Result<String, failure::Error> {
Self::check_wasm_pack_latest_version().map(|crt| crt.crt.max_version)
}
fn return_stamp_file_value(file: &str, word: &str) -> Option<String> {
let created = file
.lines()
.find(|line| line.starts_with(word))
.and_then(|l| l.split_whitespace().nth(1));
created.map(|s| s.to_string())
}
fn check_wasm_pack_latest_version() -> Result<Crate, Error> {
let url = "https://crates.io/api/v1/crates/wasm-pack";
let mut easy = easy::Easy2::new(Collector(Vec::new()));
easy.useragent(&format!(
"wasm-pack/{} ({})",
WASM_PACK_VERSION.unwrap_or_else(|| "unknown"),
WASM_PACK_REPO_URL
))?;
easy.url(url)?;
easy.get(true)?;
easy.perform()?;
let status_code = easy.response_code()?;
if 200 <= status_code && status_code < 300 {
let contents = easy.get_ref();
let result = String::from_utf8_lossy(&contents.0);
Ok(serde_json::from_str(result.into_owned().as_str())?)
} else {
bail!(
"Received a bad HTTP status code ({}) when checking for newer wasm-pack version at: {}",
status_code,
url
)
}
}
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
enum CargoWasmPackProfileWasmOpt {
Enabled(bool),
ExplicitArgs(Vec<String>),
}
impl Default for CargoWasmPackProfileWasmOpt {
fn default() -> Self {
CargoWasmPackProfileWasmOpt::Enabled(false)
}
}
impl CargoWasmPackProfile {
fn default_dev() -> Self {
CargoWasmPackProfile {
wasm_bindgen: CargoWasmPackProfileWasmBindgen {
debug_js_glue: Some(true),
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: None,
}
}
fn default_release() -> Self {
CargoWasmPackProfile {
wasm_bindgen: CargoWasmPackProfileWasmBindgen {
debug_js_glue: Some(false),
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: Some(CargoWasmPackProfileWasmOpt::Enabled(true)),
}
}
fn default_profiling() -> Self {
CargoWasmPackProfile {
wasm_bindgen: CargoWasmPackProfileWasmBindgen {
debug_js_glue: Some(false),
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: Some(CargoWasmPackProfileWasmOpt::Enabled(true)),
}
}
fn deserialize_dev<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut profile = <Option<Self>>::deserialize(deserializer)?.unwrap_or_default();
profile.update_with_defaults(&Self::default_dev());
Ok(profile)
}
fn deserialize_release<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut profile = <Option<Self>>::deserialize(deserializer)?.unwrap_or_default();
profile.update_with_defaults(&Self::default_release());
Ok(profile)
}
fn deserialize_profiling<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut profile = <Option<Self>>::deserialize(deserializer)?.unwrap_or_default();
profile.update_with_defaults(&Self::default_profiling());
Ok(profile)
}
fn update_with_defaults(&mut self, defaults: &Self) {
macro_rules! d {
( $( $path:ident ).* ) => {
self. $( $path ).* .get_or_insert(defaults. $( $path ).* .unwrap());
}
}
d!(wasm_bindgen.debug_js_glue);
d!(wasm_bindgen.demangle_name_section);
d!(wasm_bindgen.dwarf_debug_info);
if self.wasm_opt.is_none() {
self.wasm_opt = defaults.wasm_opt.clone();
}
}
pub fn wasm_bindgen_debug_js_glue(&self) -> bool {
self.wasm_bindgen.debug_js_glue.unwrap()
}
pub fn wasm_bindgen_demangle_name_section(&self) -> bool {
self.wasm_bindgen.demangle_name_section.unwrap()
}
pub fn wasm_bindgen_dwarf_debug_info(&self) -> bool {
self.wasm_bindgen.dwarf_debug_info.unwrap()
}
pub fn wasm_opt_args(&self) -> Option<Vec<String>> {
match self.wasm_opt.as_ref()? {
CargoWasmPackProfileWasmOpt::Enabled(false) => None,
CargoWasmPackProfileWasmOpt::Enabled(true) => Some(vec!["-O".to_string()]),
CargoWasmPackProfileWasmOpt::ExplicitArgs(s) => Some(s.clone()),
}
}
}
struct NpmData {
name: String,
files: Vec<String>,
dts_file: Option<String>,
main: String,
homepage: Option<String>, keywords: Option<Vec<String>>, }
#[doc(hidden)]
pub struct ManifestAndUnsedKeys {
pub manifest: CargoManifest,
pub unused_keys: BTreeSet<String>,
}
impl CrateData {
pub fn new(crate_path: &Path, out_name: Option<String>) -> Result<CrateData, Error> {
let manifest_path = crate_path.join("Cargo.toml");
if !manifest_path.is_file() {
bail!(
"crate directory is missing a `Cargo.toml` file; is `{}` the \
wrong directory?",
crate_path.display()
)
}
let data = cargo_metadata::MetadataCommand::new()
.manifest_path(&manifest_path)
.exec()?;
let manifest_and_keys = CrateData::parse_crate_data(&manifest_path)?;
CrateData::warn_for_unused_keys(&manifest_and_keys);
let manifest = manifest_and_keys.manifest;
let current_idx = data
.packages
.iter()
.position(|pkg| {
pkg.name == manifest.package.name
&& CrateData::is_same_path(&pkg.manifest_path, &manifest_path)
})
.ok_or_else(|| format_err!("failed to find package in metadata"))?;
Ok(CrateData {
data,
manifest,
current_idx,
out_name,
})
}
fn is_same_path(path1: &Path, path2: &Path) -> bool {
if let Ok(path1) = fs::canonicalize(&path1) {
if let Ok(path2) = fs::canonicalize(&path2) {
return path1 == path2;
}
}
path1 == path2
}
pub fn parse_crate_data(manifest_path: &Path) -> Result<ManifestAndUnsedKeys, Error> {
let manifest = fs::read_to_string(&manifest_path)
.with_context(|_| format!("failed to read: {}", manifest_path.display()))?;
let manifest = &mut toml::Deserializer::new(&manifest);
let mut unused_keys = BTreeSet::new();
let levenshtein_threshold = 1;
let manifest: CargoManifest = serde_ignored::deserialize(manifest, |path| {
let path_string = path.to_string();
if path_string.starts_with("package.metadata")
&& (path_string.contains("wasm-pack")
|| levenshtein(WASM_PACK_METADATA_KEY, &path_string) <= levenshtein_threshold)
{
unused_keys.insert(path_string);
}
})
.with_context(|_| format!("failed to parse manifest: {}", manifest_path.display()))?;
Ok(ManifestAndUnsedKeys {
manifest,
unused_keys,
})
}
pub fn warn_for_unused_keys(manifest_and_keys: &ManifestAndUnsedKeys) {
manifest_and_keys.unused_keys.iter().for_each(|path| {
PBAR.warn(&format!(
"\"{}\" is an unknown key and will be ignored. Please check your Cargo.toml.",
path
));
});
}
pub fn configured_profile(&self, profile: BuildProfile) -> &CargoWasmPackProfile {
match profile {
BuildProfile::Dev => &self.manifest.package.metadata.wasm_pack.profile.dev,
BuildProfile::Profiling => &self.manifest.package.metadata.wasm_pack.profile.profiling,
BuildProfile::Release => &self.manifest.package.metadata.wasm_pack.profile.release,
}
}
pub fn check_crate_config(&self) -> Result<(), Error> {
self.check_crate_type()?;
Ok(())
}
fn check_crate_type(&self) -> Result<(), Error> {
let pkg = &self.data.packages[self.current_idx];
let any_cdylib = pkg
.targets
.iter()
.filter(|target| target.kind.iter().any(|k| k == "cdylib"))
.any(|target| target.crate_types.iter().any(|s| s == "cdylib"));
if any_cdylib {
return Ok(());
}
bail!(
"crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your \
Cargo.toml file:\n\n\
[lib]\n\
crate-type = [\"cdylib\", \"rlib\"]"
)
}
pub fn crate_name(&self) -> String {
let pkg = &self.data.packages[self.current_idx];
match pkg
.targets
.iter()
.find(|t| t.kind.iter().any(|k| k == "cdylib"))
{
Some(lib) => lib.name.replace("-", "_"),
None => pkg.name.replace("-", "_"),
}
}
pub fn name_prefix(&self) -> String {
match &self.out_name {
Some(value) => value.clone(),
None => self.crate_name(),
}
}
pub fn crate_license(&self) -> &Option<String> {
&self.manifest.package.license
}
pub fn crate_license_file(&self) -> &Option<String> {
&self.manifest.package.license_file
}
pub fn target_directory(&self) -> &Path {
Path::new(&self.data.target_directory)
}
pub fn workspace_root(&self) -> &Path {
Path::new(&self.data.workspace_root)
}
pub fn write_package_json(
&self,
out_dir: &Path,
scope: &Option<String>,
disable_dts: bool,
target: Target,
) -> Result<(), Error> {
let pkg_file_path = out_dir.join("package.json");
let existing_deps = if pkg_file_path.exists() {
Some(serde_json::from_str::<HashMap<String, String>>(
&fs::read_to_string(&pkg_file_path)?,
)?)
} else {
None
};
let npm_data = match target {
Target::Nodejs => self.to_commonjs(scope, disable_dts, existing_deps, out_dir),
Target::NoModules => self.to_nomodules(scope, disable_dts, existing_deps, out_dir),
Target::Bundler => self.to_esmodules(scope, disable_dts, existing_deps, out_dir),
Target::Web => self.to_web(scope, disable_dts, existing_deps, out_dir),
};
let npm_json = serde_json::to_string_pretty(&npm_data)?;
fs::write(&pkg_file_path, npm_json)
.with_context(|_| format!("failed to write: {}", pkg_file_path.display()))?;
Ok(())
}
fn npm_data(
&self,
scope: &Option<String>,
add_js_bg_to_package_json: bool,
disable_dts: bool,
out_dir: &Path,
) -> NpmData {
let name_prefix = self.name_prefix();
let wasm_file = format!("{}_bg.wasm", name_prefix);
let js_file = format!("{}.js", name_prefix);
let mut files = vec![wasm_file];
files.push(js_file.clone());
if add_js_bg_to_package_json {
let js_bg_file = format!("{}_bg.js", name_prefix);
files.push(js_bg_file);
}
let pkg = &self.data.packages[self.current_idx];
let npm_name = match scope {
Some(s) => format!("@{}/{}", s, pkg.name),
None => pkg.name.clone(),
};
let dts_file = if !disable_dts {
let file = format!("{}.d.ts", name_prefix);
files.push(file.to_string());
Some(file)
} else {
None
};
let keywords = if pkg.keywords.len() > 0 {
Some(pkg.keywords.clone())
} else {
None
};
if let Ok(entries) = fs::read_dir(out_dir) {
let file_names = entries
.filter_map(|e| e.ok())
.filter(|e| e.metadata().map(|m| m.is_file()).unwrap_or(false))
.filter_map(|e| e.file_name().into_string().ok())
.filter(|f| f.starts_with("LICENSE"))
.filter(|f| f != "LICENSE");
for file_name in file_names {
files.push(file_name);
}
}
NpmData {
name: npm_name,
dts_file,
files,
main: js_file,
homepage: self.manifest.package.homepage.clone(),
keywords,
}
}
fn license(&self) -> Option<String> {
self.manifest.package.license.clone().or_else(|| {
self.manifest.package.license_file.clone().map(|file| {
format!("SEE LICENSE IN {}", file)
})
})
}
fn to_commonjs(
&self,
scope: &Option<String>,
disable_dts: bool,
dependencies: Option<HashMap<String, String>>,
out_dir: &Path,
) -> NpmPackage {
let data = self.npm_data(scope, false, disable_dts, out_dir);
let pkg = &self.data.packages[self.current_idx];
self.check_optional_fields();
NpmPackage::CommonJSPackage(CommonJSPackage {
name: data.name,
collaborators: pkg.authors.clone(),
description: self.manifest.package.description.clone(),
version: pkg.version.to_string(),
license: self.license(),
repository: self
.manifest
.package
.repository
.clone()
.map(|repo_url| Repository {
ty: "git".to_string(),
url: repo_url,
}),
files: data.files,
main: data.main,
homepage: data.homepage,
types: data.dts_file,
keywords: data.keywords,
dependencies,
})
}
fn to_esmodules(
&self,
scope: &Option<String>,
disable_dts: bool,
dependencies: Option<HashMap<String, String>>,
out_dir: &Path,
) -> NpmPackage {
let data = self.npm_data(scope, true, disable_dts, out_dir);
let pkg = &self.data.packages[self.current_idx];
self.check_optional_fields();
NpmPackage::ESModulesPackage(ESModulesPackage {
name: data.name,
collaborators: pkg.authors.clone(),
description: self.manifest.package.description.clone(),
version: pkg.version.to_string(),
license: self.license(),
repository: self
.manifest
.package
.repository
.clone()
.map(|repo_url| Repository {
ty: "git".to_string(),
url: repo_url,
}),
files: data.files,
module: data.main,
homepage: data.homepage,
types: data.dts_file,
side_effects: false,
keywords: data.keywords,
dependencies,
})
}
fn to_web(
&self,
scope: &Option<String>,
disable_dts: bool,
dependencies: Option<HashMap<String, String>>,
out_dir: &Path,
) -> NpmPackage {
let data = self.npm_data(scope, false, disable_dts, out_dir);
let pkg = &self.data.packages[self.current_idx];
self.check_optional_fields();
NpmPackage::ESModulesPackage(ESModulesPackage {
name: data.name,
collaborators: pkg.authors.clone(),
description: self.manifest.package.description.clone(),
version: pkg.version.to_string(),
license: self.license(),
repository: self
.manifest
.package
.repository
.clone()
.map(|repo_url| Repository {
ty: "git".to_string(),
url: repo_url,
}),
files: data.files,
module: data.main,
homepage: data.homepage,
types: data.dts_file,
side_effects: false,
keywords: data.keywords,
dependencies,
})
}
fn to_nomodules(
&self,
scope: &Option<String>,
disable_dts: bool,
dependencies: Option<HashMap<String, String>>,
out_dir: &Path,
) -> NpmPackage {
let data = self.npm_data(scope, false, disable_dts, out_dir);
let pkg = &self.data.packages[self.current_idx];
self.check_optional_fields();
NpmPackage::NoModulesPackage(NoModulesPackage {
name: data.name,
collaborators: pkg.authors.clone(),
description: self.manifest.package.description.clone(),
version: pkg.version.to_string(),
license: self.license(),
repository: self
.manifest
.package
.repository
.clone()
.map(|repo_url| Repository {
ty: "git".to_string(),
url: repo_url,
}),
files: data.files,
browser: data.main,
homepage: data.homepage,
types: data.dts_file,
keywords: data.keywords,
dependencies,
})
}
fn check_optional_fields(&self) {
let mut messages = vec![];
if self.manifest.package.description.is_none() {
messages.push("description");
}
if self.manifest.package.repository.is_none() {
messages.push("repository");
}
if self.manifest.package.license.is_none() && self.manifest.package.license_file.is_none() {
messages.push("license");
}
match messages.len() {
1 => PBAR.info(&format!("Optional field missing from Cargo.toml: '{}'. This is not necessary, but recommended", messages[0])),
2 => PBAR.info(&format!("Optional fields missing from Cargo.toml: '{}', '{}'. These are not necessary, but recommended", messages[0], messages[1])),
3 => PBAR.info(&format!("Optional fields missing from Cargo.toml: '{}', '{}', and '{}'. These are not necessary, but recommended", messages[0], messages[1], messages[2])),
_ => ()
};
}
}