Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,17 +515,50 @@ export async function build(options: BuildOptions): Promise<void> {
})).filter((p) => p.kind === "bin").map((p) => p.path),
);

const writeBinaryFile = (filePath: string, bytes: Uint8Array) => {
const dir = path.dirname(filePath);
if (!createdDirectories.has(dir)) {
Deno.mkdirSync(dir, { recursive: true });
createdDirectories.add(dir);
}
Deno.writeFileSync(
filePath,
bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes),
);
};

for (
const outputFile of [
...transformOutput.main.files,
...transformOutput.test.files,
]
) {
if (outputFile.bytes) {
const srcPath = path.join(options.outDir, "src", outputFile.filePath);
const esmPath = path.join(options.outDir, "esm", outputFile.filePath);
const scriptPath = path.join(
options.outDir,
"script",
outputFile.filePath,
);
if (!options.skipSourceOutput) {
writeBinaryFile(srcPath, outputFile.bytes);
}
if (options.esModule) {
writeBinaryFile(esmPath, outputFile.bytes);
}
if (options.scriptModule) {
writeBinaryFile(scriptPath, outputFile.bytes);
}
continue;
}

const outputFilePath = path.join(
options.outDir,
"src",
outputFile.filePath,
);

const outputFileText = binaryEntryPointPaths.has(outputFile.filePath)
? `#!/usr/bin/env node\n${
outputFile.fileText.replace(/^#![^\n\r]*\r?\n?/, "")
Expand Down
1 change: 1 addition & 0 deletions rs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ serde = { version = "1.0.159", features = ["derive"], optional = true }
serde_json.workspace = true
sys_traits.workspace = true
url.workspace = true
wasm_dep_analyzer = "0.4.0"

[dev-dependencies]
pretty_assertions = "1.3.0"
Expand Down
137 changes: 130 additions & 7 deletions rs-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub use deno_graph::source::LoaderChecksum;

use crate::declaration_file_resolution::TypesDependency;
use crate::utils::strip_bom;
use crate::utils::with_extension;

mod analyze;
mod declaration_file_resolution;
Expand All @@ -83,6 +84,8 @@ mod visitors;
pub struct OutputFile {
pub file_path: PathBuf,
pub file_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes: Option<Vec<u8>>,
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
Expand Down Expand Up @@ -462,7 +465,7 @@ pub async fn transform(
let (specifier_mappings, specifier_mapping_keys) =
resolve_specifier_mappings(
options.specifier_mappings,
&deno_resolver,
deno_resolver,
&cjs_tracker,
options
.entry_points
Expand Down Expand Up @@ -683,10 +686,46 @@ pub async fn transform(
Module::Json(module) => {
format!("export default {};", strip_bom(&module.source.text).trim(),)
}
Module::Node(_)
| Module::Npm(_)
| Module::External(_)
| Module::Wasm(_) => {
Module::Wasm(wasm_module) => {
let file_path = mappings.get_file_path(specifier).to_owned();
env_context.environment.files.push(OutputFile {
file_path: file_path.clone(),
file_text: String::new(),
bytes: Some(wasm_module.source.to_vec()),
});

let export_names = wasm_dep_analyzer::WasmDeps::parse(
&wasm_module.source,
wasm_dep_analyzer::ParseOptions::default(),
)
.ok()
.map(|deps| {
deps
.exports
.into_iter()
.map(|e| e.name.to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();

let wasm_filename = file_path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();

let wrapper_file_text =
generate_wasm_js_wrapper(&wasm_filename, &export_names);
let wrapper_file_path = with_extension(&file_path, "wasm.js");

env_context.environment.files.push(OutputFile {
file_path: wrapper_file_path,
file_text: wrapper_file_text,
bytes: None,
});

continue;
}
Module::Node(_) | Module::Npm(_) | Module::External(_) => {
bail!("Not implemented module kind for {}", module.specifier())
}
};
Expand All @@ -695,6 +734,7 @@ pub async fn transform(
env_context.environment.files.push(OutputFile {
file_path,
file_text,
bytes: None,
});
}

Expand Down Expand Up @@ -850,6 +890,7 @@ fn check_add_polyfill_file_to_environment(
env_context.environment.files.push(OutputFile {
file_path: polyfill_file_path.to_path_buf(),
file_text: polyfill_file_text,
bytes: None,
});

for entry_point in env_context.environment.entry_points.iter() {
Expand Down Expand Up @@ -895,6 +936,7 @@ fn check_add_shim_file_to_environment(
env_context.environment.files.push(OutputFile {
file_path: shim_file_path.to_path_buf(),
file_text: shim_file_text,
bytes: None,
});

for shim in env_context.shims.iter() {
Expand Down Expand Up @@ -969,7 +1011,7 @@ fn check_add_shim_file_to_environment(
.map(get_specifer_text)
.collect::<Vec<_>>()
.join(", "),
&module_specifier_text,
module_specifier_text,
));
}

Expand All @@ -981,7 +1023,7 @@ fn check_add_shim_file_to_environment(
.map(get_specifer_text)
.collect::<Vec<_>>()
.join(", "),
&module_specifier_text,
module_specifier_text,
));
}

Expand Down Expand Up @@ -1230,6 +1272,87 @@ fn get_declaration_warnings(specifiers: &Specifiers) -> Vec<String> {
}
}

fn generate_wasm_js_wrapper(
wasm_filename: &str,
export_names: &[String],
) -> String {
let mut text = String::new();
text.push_str("// Generated by dnt for WASM module wrapper\n");
text.push_str("function getNodeModule(name) {\n");
text.push_str(" if (typeof require !== \"undefined\") {\n");
text.push_str(" return require(name);\n");
text.push_str(" }\n");
text.push_str(" if (typeof process !== \"undefined\" && typeof process.getBuiltinModule === \"function\") {\n");
text.push_str(" return process.getBuiltinModule(name);\n");
text.push_str(" }\n");
text.push_str(" if (typeof module !== \"undefined\" && typeof module.createRequire === \"function\") {\n");
text.push_str(" return module.createRequire(import.meta.url)(name);\n");
text.push_str(" }\n");
text.push_str(" return null;\n");
text.push_str("}\n\n");

text.push_str("function getWasmBytes() {\n");
text.push_str(
" if (typeof process !== \"undefined\" && process?.versions?.node) {\n",
);
text.push_str(" const fs = getNodeModule(\"node:fs\");\n");
text.push_str(" const path = getNodeModule(\"node:path\");\n");
text.push_str(" const url = getNodeModule(\"node:url\");\n");
text.push_str(" if (fs && path && url) {\n");
text.push_str(" const dir = typeof __dirname !== \"undefined\" ? __dirname : path.dirname(url.fileURLToPath(import.meta.url));\n");
text.push_str(&format!(
" return fs.readFileSync(path.resolve(dir, \"./{wasm_filename}\"));\n"
));
text.push_str(" }\n");
text.push_str(" }\n");
text.push_str(" if (typeof XMLHttpRequest !== \"undefined\") {\n");
text.push_str(" const req = new XMLHttpRequest();\n");
text.push_str(&format!(
" req.open(\"GET\", new URL(\"./{wasm_filename}\", import.meta.url), false);\n"
));
text.push_str(" req.responseType = \"arraybuffer\";\n");
text.push_str(" req.send(null);\n");
text.push_str(" if (req.status === 200 || req.status === 0) {\n");
text.push_str(" return new Uint8Array(req.response);\n");
text.push_str(" }\n");
text.push_str(" }\n");
text.push_str(&format!(
" throw new Error(\"Unable to load WASM file '{wasm_filename}' in current environment\");\n"
));
text.push_str("}\n\n");

text.push_str("const wasmBytes = getWasmBytes();\n");
text.push_str("const wasmModule = new WebAssembly.Module(wasmBytes);\n");
text.push_str(
"const wasmInstance = new WebAssembly.Instance(wasmModule, {});\n\n",
);

text.push_str("export default wasmInstance.exports;\n");

for name in export_names {
if is_valid_js_identifier(name) {
text.push_str(&format!(
"export const {name} = wasmInstance.exports.{name};\n"
));
}
}

text
}

fn is_valid_js_identifier(name: &str) -> bool {
if name.is_empty() || name == "default" {
return false;
}
let mut chars = name.chars();
if let Some(first) = chars.next() {
if !first.is_alphabetic() && first != '_' && first != '$' {
return false;
}
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}

#[cfg(test)]
mod test {

Expand Down
2 changes: 1 addition & 1 deletion rs-lib/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl<'a> SourceLoader<'a> {
fn mapping(
&self,
specifier: &ModuleSpecifier,
) -> Option<Cow<MappedSpecifier>> {
) -> Option<Cow<'_, MappedSpecifier>> {
match self.specifier_mappings.get(specifier) {
Some(mapping) => Some(Cow::Borrowed(mapping)),
None => self.jsr_specifier_mappings.get(specifier).map(Cow::Owned),
Expand Down
2 changes: 2 additions & 0 deletions rs-lib/src/mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,10 +494,12 @@ fn get_mapped_file_path(
let filepath_no_ext =
get_unique_path(without_ext(path.as_ref()), mapped_filepaths_no_ext);
let extension = match media_type {
MediaType::Wasm => "wasm",
MediaType::Json => "js",
MediaType::Mjs | MediaType::Mts => "js",
_ => &media_type.as_ts_extension()[1..],
};

with_extension(
&filepath_no_ext,
&if let Some(sub_ext) = filepath_no_ext.extension() {
Expand Down
16 changes: 6 additions & 10 deletions rs-lib/src/specifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub fn get_specifiers<'a>(

for module in all_modules.iter() {
match module {
Module::Js(_) | Module::Json(_) => {
Module::Js(_) | Module::Json(_) | Module::Wasm(_) => {
match module.specifier().scheme().to_lowercase().as_str() {
"file" => local_specifiers.push(module.specifier().clone()),
"http" | "https" => {
Expand All @@ -137,12 +137,7 @@ pub fn get_specifiers<'a>(
Module::Npm(_) | Module::Node(_) => {
// ignore
}
Module::Wasm(_) => {
anyhow::bail!(
"Not implemented support for Wasm modules: {}",
module.specifier()
);
}

Module::External(module) => {
let specifier = &module.specifier;
if let Ok(npm_specifier) =
Expand Down Expand Up @@ -191,11 +186,11 @@ pub fn get_specifiers<'a>(
Ok(Specifiers {
local: local_specifiers
.into_iter()
.filter(|l| !declaration_specifiers.contains(&l))
.filter(|l| !declaration_specifiers.contains(l))
.collect(),
remote: remote_specifiers
.into_iter()
.filter(|l| !declaration_specifiers.contains(&l))
.filter(|l| !declaration_specifiers.contains(l))
.collect(),
types,
types_packages: declaration_files.types_packages,
Expand Down Expand Up @@ -249,7 +244,8 @@ fn get_reachable(
roots: &[ModuleSpecifier],
) -> HashSet<ModuleSpecifier> {
let mut found = HashSet::new();
let mut pending = roots.iter().cloned().collect::<Vec<_>>();
let mut pending = roots.to_vec();

while let Some(specifier) = pending.pop() {
let specifier = module_graph.resolve(&specifier).clone();
if !found.insert(specifier.clone()) {
Expand Down
22 changes: 9 additions & 13 deletions rs-lib/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,15 @@ pub fn get_relative_specifier(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> String {
let to = with_extension(
to.as_ref(),
if to
.as_ref()
.to_string_lossy()
.to_lowercase()
.ends_with(".d.ts")
{
""
} else {
"js"
},
);
let to_str = to.as_ref().to_string_lossy().to_lowercase();
let to = if to_str.ends_with(".wasm") {
with_extension(to.as_ref(), "wasm.js")
} else if to_str.ends_with(".d.ts") {
with_extension(to.as_ref(), "")
} else {
with_extension(to.as_ref(), "js")
};

let relative_path = get_relative_path(from, to);
let relative_path_str = relative_path
.to_string_lossy()
Expand Down
Loading