From c747d9598951dfa18e3b4983405b8f762fa046e6 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Tue, 25 Nov 2025 12:32:55 +0100 Subject: [PATCH 1/5] node:path implementation, first version --- .../wasm-rquickjs/skeleton/src/builtin/mod.rs | 7 + .../skeleton/src/builtin/path.js | 111 +++++++ .../skeleton/src/builtin/path.rs | 289 ++++++++++++++++++ examples/path/src/path.js | 124 ++++++++ examples/path/wit/path.wit | 13 + tests/runtime.rs | 88 ++++++ 6 files changed, 632 insertions(+) create mode 100644 crates/wasm-rquickjs/skeleton/src/builtin/path.js create mode 100644 crates/wasm-rquickjs/skeleton/src/builtin/path.rs create mode 100644 examples/path/src/path.js create mode 100644 examples/path/wit/path.wit diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/mod.rs b/crates/wasm-rquickjs/skeleton/src/builtin/mod.rs index d67907a..862d8f1 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/mod.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/mod.rs @@ -19,6 +19,7 @@ mod http { mod eventemitter; mod ieee754; mod internal; +mod path; mod process; mod stream; mod string_decoder; @@ -56,6 +57,9 @@ pub fn add_module_resolvers( .with_module("__wasm_rquickjs_builtin/process_native") .with_module("node:process") .with_module("process") + .with_module("__wasm_rquickjs_builtin/path_native") + .with_module("node:path") + .with_module("path") .with_module("__wasm_rquickjs_builtin/url_native") .with_module("__wasm_rquickjs_builtin/url") .with_module("node:events") @@ -99,6 +103,7 @@ pub fn module_loader() -> ( "__wasm_rquickjs_builtin/process_native", process::js_native_module, ) + .with_module("__wasm_rquickjs_builtin/path_native", path::js_native_module) .with_module("__wasm_rquickjs_builtin/url_native", url::js_native_module) .with_module( "__wasm_rquickjs_builtin/web_crypto_native", @@ -122,6 +127,8 @@ pub fn module_loader() -> ( .with_module("fs", fs::FS_JS) .with_module("node:process", process::PROCESS_JS) .with_module("process", process::PROCESS_JS) + .with_module("node:path", path::PATH_JS) + .with_module("path", path::PATH_JS) .with_module("__wasm_rquickjs_builtin/url", url::URL_JS) .with_module("node:events", eventemitter::EVENTEMITTER_JS) .with_module("events", eventemitter::EVENTEMITTER_JS) diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.js b/crates/wasm-rquickjs/skeleton/src/builtin/path.js new file mode 100644 index 0000000..e1528d9 --- /dev/null +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.js @@ -0,0 +1,111 @@ +// node:path implementation (POSIX only) +// Provides utilities for working with file and directory paths + +import { + basename, + dirname, + extname, + is_absolute as isAbsolute, + join as joinNative, + normalize, + relative, + resolve as resolveNative, + parse as parseNative, + format as formatNative, + create_parsed_path as createParsedPath, +} from "__wasm_rquickjs_builtin/path_native"; + +// Match a path against a glob pattern +function matchesGlob(path, pattern) { + if (typeof path !== 'string' || typeof pattern !== 'string') { + throw new TypeError('Both path and pattern must be strings'); + } + + // Simple glob matching: only supports * and ? wildcards + const regexStr = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars + .replace(/\*/g, '.*') // * matches any sequence + .replace(/\?/g, '.'); // ? matches single char + + const regex = new RegExp(`^${regexStr}$`); + return regex.test(path); +} + +// Convert to namespaced path (no-op on POSIX) +function toNamespacedPath(path) { + if (typeof path !== 'string') { + return path; + } + return path; +} + +function join(...paths) { + return joinNative(paths); +} + +function resolve(...paths) { + return resolveNative(paths); +} + +function parse(path) { + return parseNative(path); +} + +function format(pathObject) { + if (pathObject === null || typeof pathObject !== 'object') { + throw new TypeError('The "pathObject" argument must be of type Object. Received ' + pathObject); + } + + // Check if it is a ParsedPath using duck typing as we don't have the class constructor easily accessible + // If we passed a ParsedPath instance from parseNative, we can probably pass it back. + // However, to be safe and support plain objects, we create a new ParsedPath instance. + + const pp = createParsedPath(); + if (pathObject.root !== undefined) pp.root = String(pathObject.root); + if (pathObject.dir !== undefined) pp.dir = String(pathObject.dir); + if (pathObject.base !== undefined) pp.base = String(pathObject.base); + if (pathObject.ext !== undefined) pp.ext = String(pathObject.ext); + if (pathObject.name !== undefined) pp.name = String(pathObject.name); + + return formatNative(pp); +} + +const sep = '/'; +const delimiter = ':'; + +// POSIX path object with all methods +const posix = { + sep, + delimiter, + basename, + dirname, + extname, + isAbsolute, + join, + normalize, + relative, + resolve, + parse, + format, + matchesGlob, + toNamespacedPath, +}; + +// Export all path functions +export { + sep, + delimiter, + basename, + dirname, + extname, + isAbsolute, + join, + normalize, + relative, + resolve, + parse, + format, + matchesGlob, + toNamespacedPath, + posix, +}; diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs new file mode 100644 index 0000000..fe219ff --- /dev/null +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs @@ -0,0 +1,289 @@ +use rquickjs::JsLifetime; +use rquickjs::class::Trace; + +// Result type for parse function +#[derive(JsLifetime, Trace, Clone, Default)] +#[rquickjs::class] +pub struct ParsedPath { + #[qjs(get, set)] + pub root: String, + #[qjs(get, set)] + pub dir: String, + #[qjs(get, set)] + pub base: String, + #[qjs(get, set)] + pub ext: String, + #[qjs(get, set)] + pub name: String, +} + +// Native functions for the node:path implementation (POSIX only) +#[rquickjs::module] +pub mod native_module { + use super::ParsedPath; + use rquickjs::prelude::*; + use std::path::Path; + + #[rquickjs::function] + pub fn create_parsed_path() -> ParsedPath { + ParsedPath::default() + } + + #[rquickjs::function] + pub fn basename(path: String, suffix: Opt) -> String { + let path = Path::new(&path); + let basename = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + if let Some(suffix) = suffix.0 { + if basename.ends_with(&suffix) { + basename[..basename.len() - suffix.len()].to_string() + } else { + basename.to_string() + } + } else { + basename.to_string() + } + } + + #[rquickjs::function] + pub fn dirname(path: String) -> String { + let path = Path::new(&path); + path.parent() + .and_then(|d| d.to_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| ".".to_string()) + } + + #[rquickjs::function] + pub fn extname(path: String) -> String { + let path = Path::new(&path); + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| format!(".{}", ext)) + .unwrap_or_default() + } + + #[rquickjs::function] + pub fn is_absolute(path: String) -> bool { + Path::new(&path).is_absolute() + } + + fn normalize_impl(path: String) -> String { + if path.is_empty() { + return ".".to_string(); + } + + let is_absolute = path.starts_with('/'); + + // Split and filter out empty segments and "." + let parts: Vec<&str> = path + .split('/') + .filter(|p| !p.is_empty() && p != &".") + .collect(); + + let mut result = Vec::new(); + for part in parts { + if part == ".." { + if !result.is_empty() { + let last = result.last().unwrap(); + if *last == ".." { + result.push(".."); + } else { + result.pop(); + } + } else if is_absolute { + // For absolute paths, .. at root is ignored + } else { + result.push(".."); + } + } else { + result.push(part); + } + } + + if result.is_empty() { + if is_absolute { + "/".to_string() + } else { + ".".to_string() + } + } else { + let normalized = result.join("/"); + if is_absolute { + format!("/{}", normalized) + } else { + normalized + } + } + } + + #[rquickjs::function] + pub fn join(paths: Vec) -> String { + if paths.is_empty() { + return ".".to_string(); + } + + let joined = paths.iter() + .filter(|p| !p.is_empty()) + .cloned() + .collect::>() + .join("/"); + + // If the result is empty (e.g. join("", "")), return "." + if joined.is_empty() { + return ".".to_string(); + } + + normalize_impl(joined) + } + + #[rquickjs::function] + pub fn normalize(path: String) -> String { + normalize_impl(path) + } + + fn resolve_impl(paths: &[String]) -> String { + let mut resolved = String::new(); + let mut resolved_absolute = false; + + for path in paths.iter().rev() { + if path.is_empty() { + continue; + } + + if resolved.is_empty() { + resolved = path.clone(); + } else { + resolved = format!("{}/{}", path, resolved); + } + + if path.starts_with('/') { + resolved_absolute = true; + break; + } + } + + if !resolved_absolute { + if resolved.is_empty() { + resolved = "/".to_string(); + } else { + resolved = format!("/{}", resolved); + } + } + + normalize_impl(resolved) + } + + #[rquickjs::function] + pub fn resolve(paths: Vec) -> String { + resolve_impl(&paths) + } + + #[rquickjs::function] + pub fn relative(from: String, to: String) -> String { + let from_res = resolve_impl(&[from]); + let to_res = resolve_impl(&[to]); + + let from_parts: Vec<&str> = from_res.split('/').filter(|s| !s.is_empty()).collect(); + let to_parts: Vec<&str> = to_res.split('/').filter(|s| !s.is_empty()).collect(); + + let mut common_len = 0; + for (f, t) in from_parts.iter().zip(to_parts.iter()) { + if f == t { + common_len += 1; + } else { + break; + } + } + + let mut result = Vec::new(); + + for _ in common_len..from_parts.len() { + result.push(".."); + } + + for i in common_len..to_parts.len() { + result.push(to_parts[i]); + } + + if result.is_empty() { + "".to_string() + } else { + result.join("/") + } + } + + #[rquickjs::function] + pub fn parse(path: String) -> ParsedPath { + if path.is_empty() { + return ParsedPath::default(); + } + + let path = Path::new(&path); + + let root = if path.has_root() { + "/".to_string() + } else { + "".to_string() + }; + + let dir = path + .parent() + .map(|p| p.to_str().unwrap_or("").to_string()) + .unwrap_or("".to_string()); + + let base = path + .file_name() + .map(|n| n.to_str().unwrap_or("").to_string()) + .unwrap_or("".to_string()); + + let ext = path + .extension() + .map(|ext| format!(".{}", ext.to_str().unwrap_or(""))) + .unwrap_or("".to_string()); + + let name = if ext.is_empty() { + base.clone() + } else { + base.strip_suffix(&ext) + .map(|r| r.to_string()) + .unwrap_or(base.clone()) + }; + + ParsedPath { + root, + dir, + base, + ext, + name, + } + } + + #[rquickjs::function] + pub fn format(path_obj: ParsedPath) -> String { + let dir = if !path_obj.dir.is_empty() { + path_obj.dir.clone() + } else { + path_obj.root.clone() + }; + + let base = if !path_obj.base.is_empty() { + path_obj.base.clone() + } else { + format!("{}{}", path_obj.name, path_obj.ext) + }; + + if dir.is_empty() { + return base; + } + + if dir == path_obj.root { + format!("{}{}", dir, base) + } else { + format!("{}/{}", dir, base) + } + } +} + +// JS functions for the path implementation +pub const PATH_JS: &str = include_str!("path.js"); diff --git a/examples/path/src/path.js b/examples/path/src/path.js new file mode 100644 index 0000000..8d159be --- /dev/null +++ b/examples/path/src/path.js @@ -0,0 +1,124 @@ +import * as path from 'node:path'; + +function assert(condition, message) { + if (!condition) { + console.log("Assertion failed: " + message); + throw new Error(message); + } +} + +export const testBasename = () => { + try { + assert(path.basename('/foo/bar/baz/asdf/quux.html') === 'quux.html', "basename 1"); + assert(path.basename('/foo/bar/baz/asdf/quux.html', '.html') === 'quux', "basename 2"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testDirname = () => { + try { + assert(path.dirname('/foo/bar/baz/asdf/quux') === '/foo/bar/baz/asdf', "dirname 1"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testExtname = () => { + try { + assert(path.extname('index.html') === '.html', "extname 1"); + assert(path.extname('index.coffee.md') === '.md', "extname 2"); + assert(path.extname('index.') === '.', "extname 3"); + assert(path.extname('index') === '', "extname 4"); + assert(path.extname('.index') === '', "extname 5"); + assert(path.extname('.index.md') === '.md', "extname 6"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testIsAbsolute = () => { + try { + assert(path.isAbsolute('/foo/bar') === true, "isAbsolute 1"); + assert(path.isAbsolute('/baz/..') === true, "isAbsolute 2"); + assert(path.isAbsolute('quux/') === false, "isAbsolute 3"); + assert(path.isAbsolute('.') === false, "isAbsolute 4"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testJoin = () => { + try { + assert(path.join('/foo', 'bar', 'baz/asdf', 'quux', '..') === '/foo/bar/baz/asdf', "join 1"); + assert(path.join('/foo', '/bar') === '/foo/bar', "join 2"); // This will fail with current impl + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testNormalize = () => { + try { + assert(path.normalize('/foo/bar//baz/asdf/quux/..') === '/foo/bar/baz/asdf', "normalize 1"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testRelative = () => { + try { + assert(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb') === '../../impl/bbb', "relative 1"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testResolve = () => { + try { + assert(path.resolve('/foo/bar', './baz') === '/foo/bar/baz', "resolve 1"); + assert(path.resolve('/foo/bar', '/tmp/file/') === '/tmp/file', "resolve 2"); + assert(path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') === '/wwwroot/static_files/gif/image.gif', "resolve 3"); // Assumes cwd is / + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testParseFormat = () => { + try { + const p = path.parse('/home/user/dir/file.txt'); + assert(p.root === '/', "parse root"); + assert(p.dir === '/home/user/dir', "parse dir"); + assert(p.base === 'file.txt', "parse base"); + assert(p.ext === '.txt', "parse ext"); + assert(p.name === 'file', "parse name"); + + const f = path.format({ + root: '/', + dir: '/home/user/dir', + base: 'file.txt', + ext: '.txt', + name: 'file' + }); + assert(f === '/home/user/dir/file.txt', "format 1"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; diff --git a/examples/path/wit/path.wit b/examples/path/wit/path.wit new file mode 100644 index 0000000..2de56cc --- /dev/null +++ b/examples/path/wit/path.wit @@ -0,0 +1,13 @@ +package quickjs:path; + +world path { + export test-basename: func() -> bool; + export test-dirname: func() -> bool; + export test-extname: func() -> bool; + export test-is-absolute: func() -> bool; + export test-join: func() -> bool; + export test-normalize: func() -> bool; + export test-relative: func() -> bool; + export test-resolve: func() -> bool; + export test-parse-format: func() -> bool; +} diff --git a/tests/runtime.rs b/tests/runtime.rs index c9d6aed..81144d5 100644 --- a/tests/runtime.rs +++ b/tests/runtime.rs @@ -1235,3 +1235,91 @@ async fn web_crypto_random_u32( _ => Err(anyhow!("Expected list result")), } } + +#[test_dep(tagged_as = "path")] +fn compiled_path() -> CompiledTest { + let path = Utf8Path::new("examples/path"); + CompiledTest::new(path, false).expect("Failed to compile path") +} + +#[test] +async fn path_test_basename(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-basename", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_dirname(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-dirname", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_extname(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-extname", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_is_absolute(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-is-absolute", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_join(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-join", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_normalize(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-normalize", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_relative(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-relative", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_resolve(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-resolve", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_parse_format(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-parse-format", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + From 55bdd439109c7e0a434b7ebd80909c330cf42c74 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Tue, 25 Nov 2025 13:17:34 +0100 Subject: [PATCH 2/5] More tests and cleanup --- .../skeleton/src/builtin/path.js | 15 ++--- .../skeleton/src/builtin/path.rs | 56 ++++++++++++------- examples/path/src/path.js | 51 ++++++++++++++++- examples/path/wit/path.wit | 2 + tests/runtime.rs | 18 ++++++ 5 files changed, 112 insertions(+), 30 deletions(-) diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.js b/crates/wasm-rquickjs/skeleton/src/builtin/path.js index e1528d9..955cb56 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/path.js +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.js @@ -12,7 +12,7 @@ import { resolve as resolveNative, parse as parseNative, format as formatNative, - create_parsed_path as createParsedPath, + ParsedPath, } from "__wasm_rquickjs_builtin/path_native"; // Match a path against a glob pattern @@ -60,12 +60,13 @@ function format(pathObject) { // If we passed a ParsedPath instance from parseNative, we can probably pass it back. // However, to be safe and support plain objects, we create a new ParsedPath instance. - const pp = createParsedPath(); - if (pathObject.root !== undefined) pp.root = String(pathObject.root); - if (pathObject.dir !== undefined) pp.dir = String(pathObject.dir); - if (pathObject.base !== undefined) pp.base = String(pathObject.base); - if (pathObject.ext !== undefined) pp.ext = String(pathObject.ext); - if (pathObject.name !== undefined) pp.name = String(pathObject.name); + const root = pathObject.root !== undefined ? String(pathObject.root) : ""; + const dir = pathObject.dir !== undefined ? String(pathObject.dir) : ""; + const base = pathObject.base !== undefined ? String(pathObject.base) : ""; + const ext = pathObject.ext !== undefined ? String(pathObject.ext) : ""; + const name = pathObject.name !== undefined ? String(pathObject.name) : ""; + + const pp = new ParsedPath(root, dir, base, ext, name); return formatNative(pp); } diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs index fe219ff..9475ebd 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs @@ -1,32 +1,41 @@ use rquickjs::JsLifetime; use rquickjs::class::Trace; -// Result type for parse function -#[derive(JsLifetime, Trace, Clone, Default)] -#[rquickjs::class] -pub struct ParsedPath { - #[qjs(get, set)] - pub root: String, - #[qjs(get, set)] - pub dir: String, - #[qjs(get, set)] - pub base: String, - #[qjs(get, set)] - pub ext: String, - #[qjs(get, set)] - pub name: String, -} - // Native functions for the node:path implementation (POSIX only) #[rquickjs::module] pub mod native_module { - use super::ParsedPath; use rquickjs::prelude::*; + use rquickjs::{JsLifetime, class::Trace}; use std::path::Path; - #[rquickjs::function] - pub fn create_parsed_path() -> ParsedPath { - ParsedPath::default() + // Result type for parse function + #[derive(JsLifetime, Trace, Clone, Default)] + #[rquickjs::class] + pub struct ParsedPath { + #[qjs(get, set)] + pub root: String, + #[qjs(get, set)] + pub dir: String, + #[qjs(get, set)] + pub base: String, + #[qjs(get, set)] + pub ext: String, + #[qjs(get, set)] + pub name: String, + } + + #[rquickjs::methods] + impl ParsedPath { + #[qjs(constructor)] + pub fn new(root: String, dir: String, base: String, ext: String, name: String) -> Self { + Self { + root, + dir, + base, + ext, + name, + } + } } #[rquickjs::function] @@ -270,7 +279,12 @@ pub mod native_module { let base = if !path_obj.base.is_empty() { path_obj.base.clone() } else { - format!("{}{}", path_obj.name, path_obj.ext) + let ext = if !path_obj.ext.is_empty() && !path_obj.ext.starts_with('.') { + format!(".{}", path_obj.ext) + } else { + path_obj.ext.clone() + }; + format!("{}{}", path_obj.name, ext) }; if dir.is_empty() { diff --git a/examples/path/src/path.js b/examples/path/src/path.js index 8d159be..0541ca0 100644 --- a/examples/path/src/path.js +++ b/examples/path/src/path.js @@ -108,14 +108,61 @@ export const testParseFormat = () => { assert(p.ext === '.txt', "parse ext"); assert(p.name === 'file', "parse name"); - const f = path.format({ + const f1 = path.format({ root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }); - assert(f === '/home/user/dir/file.txt', "format 1"); + assert(f1 === '/home/user/dir/file.txt', "format 1"); + + const f2 = path.format({ + root: '/', + base: 'file.txt', + ext: 'ignored' + }); + assert(f2 === '/file.txt', "format 2"); + + const f3 = path.format({ + root: '/', + name: 'file', + ext: '.txt' + }); + assert(f3 === '/file.txt', "format 3"); + + const f4 = path.format({ + root: '/', + name: 'file', + ext: 'txt' + }); + assert(f4 === '/file.txt', "format 4"); + + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testDelimiter = () => { + try { + assert(path.delimiter === ':', "delimiter"); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export const testSep = () => { + try { + assert(path.sep === '/', "sep"); + const parts = 'foo/bar/baz'.split(path.sep); + assert(parts.length === 3, "sep split length"); + assert(parts[0] === 'foo', "sep split 0"); + assert(parts[1] === 'bar', "sep split 1"); + assert(parts[2] === 'baz', "sep split 2"); return true; } catch (e) { console.error(e); diff --git a/examples/path/wit/path.wit b/examples/path/wit/path.wit index 2de56cc..c8cb8c6 100644 --- a/examples/path/wit/path.wit +++ b/examples/path/wit/path.wit @@ -10,4 +10,6 @@ world path { export test-relative: func() -> bool; export test-resolve: func() -> bool; export test-parse-format: func() -> bool; + export test-delimiter: func() -> bool; + export test-sep: func() -> bool; } diff --git a/tests/runtime.rs b/tests/runtime.rs index 81144d5..2dd4c2b 100644 --- a/tests/runtime.rs +++ b/tests/runtime.rs @@ -1323,3 +1323,21 @@ async fn path_test_parse_format(#[tagged_as("path")] compiled_test: &CompiledTes Ok(()) } +#[test] +async fn path_test_delimiter(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-delimiter", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + +#[test] +async fn path_test_sep(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { + let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-sep", &[]).await; + let r = r?; + println!("Output:\n{}", output); + assert_eq!(r, Some(Val::Bool(true))); + Ok(()) +} + From c80db13216a8c18023b4d9f8631af711c3f3c459 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Tue, 25 Nov 2025 13:17:43 +0100 Subject: [PATCH 3/5] Format --- tests/runtime.rs | 70 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/tests/runtime.rs b/tests/runtime.rs index 2dd4c2b..5a14756 100644 --- a/tests/runtime.rs +++ b/tests/runtime.rs @@ -1243,8 +1243,11 @@ fn compiled_path() -> CompiledTest { } #[test] -async fn path_test_basename(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-basename", &[]).await; +async fn path_test_basename( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-basename", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1252,8 +1255,11 @@ async fn path_test_basename(#[tagged_as("path")] compiled_test: &CompiledTest) - } #[test] -async fn path_test_dirname(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-dirname", &[]).await; +async fn path_test_dirname( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-dirname", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1261,8 +1267,11 @@ async fn path_test_dirname(#[tagged_as("path")] compiled_test: &CompiledTest) -> } #[test] -async fn path_test_extname(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-extname", &[]).await; +async fn path_test_extname( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-extname", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1270,8 +1279,11 @@ async fn path_test_extname(#[tagged_as("path")] compiled_test: &CompiledTest) -> } #[test] -async fn path_test_is_absolute(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-is-absolute", &[]).await; +async fn path_test_is_absolute( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-is-absolute", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1280,7 +1292,8 @@ async fn path_test_is_absolute(#[tagged_as("path")] compiled_test: &CompiledTest #[test] async fn path_test_join(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-join", &[]).await; + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-join", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1288,8 +1301,11 @@ async fn path_test_join(#[tagged_as("path")] compiled_test: &CompiledTest) -> an } #[test] -async fn path_test_normalize(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-normalize", &[]).await; +async fn path_test_normalize( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-normalize", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1297,8 +1313,11 @@ async fn path_test_normalize(#[tagged_as("path")] compiled_test: &CompiledTest) } #[test] -async fn path_test_relative(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-relative", &[]).await; +async fn path_test_relative( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-relative", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1306,8 +1325,11 @@ async fn path_test_relative(#[tagged_as("path")] compiled_test: &CompiledTest) - } #[test] -async fn path_test_resolve(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-resolve", &[]).await; +async fn path_test_resolve( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-resolve", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1315,8 +1337,11 @@ async fn path_test_resolve(#[tagged_as("path")] compiled_test: &CompiledTest) -> } #[test] -async fn path_test_parse_format(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-parse-format", &[]).await; +async fn path_test_parse_format( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-parse-format", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1324,8 +1349,11 @@ async fn path_test_parse_format(#[tagged_as("path")] compiled_test: &CompiledTes } #[test] -async fn path_test_delimiter(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-delimiter", &[]).await; +async fn path_test_delimiter( + #[tagged_as("path")] compiled_test: &CompiledTest, +) -> anyhow::Result<()> { + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-delimiter", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); @@ -1334,10 +1362,10 @@ async fn path_test_delimiter(#[tagged_as("path")] compiled_test: &CompiledTest) #[test] async fn path_test_sep(#[tagged_as("path")] compiled_test: &CompiledTest) -> anyhow::Result<()> { - let (r, output) = invoke_and_capture_output(compiled_test.wasm_path(), None, "test-sep", &[]).await; + let (r, output) = + invoke_and_capture_output(compiled_test.wasm_path(), None, "test-sep", &[]).await; let r = r?; println!("Output:\n{}", output); assert_eq!(r, Some(Val::Bool(true))); Ok(()) } - From 216df00b098b7ff69e3d314ee5e80fecdaced782 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Tue, 25 Nov 2025 13:53:59 +0100 Subject: [PATCH 4/5] Fix --- README.md | 33 ++++++++++ .../skeleton/src/builtin/path.rs | 61 ++++++++++--------- .../generated_types_path_exports.d.ts | 13 ++++ 3 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 tests/goldenfiles/generated_types_path_exports.d.ts diff --git a/README.md b/README.md index a7fc639..65e5424 100644 --- a/README.md +++ b/README.md @@ -352,12 +352,45 @@ Implemented by https://github.com/MattiasBuelens/web-streams-polyfill - `writeFile` - `writeFileSync` +### `node:path` +- `sep` +- `delimiter` +- `basename` +- `dirname` +- `extname` +- `isAbsolute` +- `join` +- `normalize` +- `relative` +- `resolve` +- `parse` +- `format` +- `matchesGlob` +- `toNamespacedPath` +- `posix` + ### `node:process` - `argv` - `argv0` - `env` - `cwd` +### `node:stream` +- `_uint8ArrayToBuffer` +- `addAbortSignal` +- `compose` +- `destroy` +- `Duplex` +- `finished` +- `isDisturbed` +- `_isUint8Array` +- `PassThrough` +- `pipeline` +- `Readable` +- `Stream` +- `Transform` +- `Writable` + ### `base64-js` - `byteLength` - `toByteArray` diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs index 9475ebd..0e31184 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs @@ -7,36 +7,7 @@ pub mod native_module { use rquickjs::prelude::*; use rquickjs::{JsLifetime, class::Trace}; use std::path::Path; - - // Result type for parse function - #[derive(JsLifetime, Trace, Clone, Default)] - #[rquickjs::class] - pub struct ParsedPath { - #[qjs(get, set)] - pub root: String, - #[qjs(get, set)] - pub dir: String, - #[qjs(get, set)] - pub base: String, - #[qjs(get, set)] - pub ext: String, - #[qjs(get, set)] - pub name: String, - } - - #[rquickjs::methods] - impl ParsedPath { - #[qjs(constructor)] - pub fn new(root: String, dir: String, base: String, ext: String, name: String) -> Self { - Self { - root, - dir, - base, - ext, - name, - } - } - } + use super::ParsedPath; #[rquickjs::function] pub fn basename(path: String, suffix: Opt) -> String { @@ -299,5 +270,35 @@ pub mod native_module { } } +// Result type for parse function +#[derive(JsLifetime, Trace, Clone, Default)] +#[rquickjs::class] +pub struct ParsedPath { + #[qjs(get, set)] + pub root: String, + #[qjs(get, set)] + pub dir: String, + #[qjs(get, set)] + pub base: String, + #[qjs(get, set)] + pub ext: String, + #[qjs(get, set)] + pub name: String, +} + +#[rquickjs::methods] +impl ParsedPath { + #[qjs(constructor)] + pub fn new(root: String, dir: String, base: String, ext: String, name: String) -> Self { + Self { + root, + dir, + base, + ext, + name, + } + } +} + // JS functions for the path implementation pub const PATH_JS: &str = include_str!("path.js"); diff --git a/tests/goldenfiles/generated_types_path_exports.d.ts b/tests/goldenfiles/generated_types_path_exports.d.ts new file mode 100644 index 0000000..1fc5c92 --- /dev/null +++ b/tests/goldenfiles/generated_types_path_exports.d.ts @@ -0,0 +1,13 @@ +declare module 'path' { + export function testBasename(): Promise; + export function testDirname(): Promise; + export function testExtname(): Promise; + export function testIsAbsolute(): Promise; + export function testJoin(): Promise; + export function testNormalize(): Promise; + export function testRelative(): Promise; + export function testResolve(): Promise; + export function testParseFormat(): Promise; + export function testDelimiter(): Promise; + export function testSep(): Promise; +} From 4245367792c570cfa975d2a061984bbe53782402 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Tue, 25 Nov 2025 14:41:08 +0100 Subject: [PATCH 5/5] Fix --- crates/wasm-rquickjs/skeleton/src/builtin/path.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs index 0e31184..622c47d 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/path.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/path.rs @@ -7,7 +7,8 @@ pub mod native_module { use rquickjs::prelude::*; use rquickjs::{JsLifetime, class::Trace}; use std::path::Path; - use super::ParsedPath; + + pub use super::ParsedPath; #[rquickjs::function] pub fn basename(path: String, suffix: Opt) -> String {