diff --git a/crates/capsule-core/src/config/fingerprint.rs b/crates/capsule-core/src/config/fingerprint.rs index 59a9daf..4d2cd16 100644 --- a/crates/capsule-core/src/config/fingerprint.rs +++ b/crates/capsule-core/src/config/fingerprint.rs @@ -12,8 +12,15 @@ pub struct SourceFingerprint { impl SourceFingerprint { const MANIFEST_FILENAME: &'static str = ".capsule_fingerprint.json"; - pub fn load(cache_dir: &Path) -> Self { - let fingerprint_path = cache_dir.join(Self::MANIFEST_FILENAME); + fn manifest_filename(key: Option<&str>) -> String { + match key { + Some(k) => format!(".capsule_fingerprint_{}.json", k), + None => Self::MANIFEST_FILENAME.to_string(), + } + } + + pub fn load(cache_dir: &Path, key: Option<&str>) -> Self { + let fingerprint_path = cache_dir.join(Self::manifest_filename(key)); if fingerprint_path.exists() && let Ok(content) = fs::read_to_string(&fingerprint_path) @@ -25,8 +32,8 @@ impl SourceFingerprint { Self::default() } - pub fn save(&self, cache_dir: &Path) -> std::io::Result<()> { - let fingerprint_path = cache_dir.join(Self::MANIFEST_FILENAME); + pub fn save(&self, cache_dir: &Path, key: Option<&str>) -> std::io::Result<()> { + let fingerprint_path = cache_dir.join(Self::manifest_filename(key)); let content = serde_json::to_string_pretty(self)?; fs::write(fingerprint_path, content) } @@ -114,14 +121,13 @@ impl SourceFingerprint { output_path: &Path, extensions: &[&str], ignored_dirs: &[&str], + key: Option<&str>, ) -> bool { if !output_path.exists() { return true; } - let previous = Self::load(cache_dir); - + let previous = Self::load(cache_dir, key); let current = Self::build(source_dir, extensions, ignored_dirs); - current.has_changes(&previous) } @@ -130,9 +136,10 @@ impl SourceFingerprint { source_dir: &Path, extensions: &[&str], ignored_dirs: &[&str], + key: Option<&str>, ) -> std::io::Result<()> { let fingerprint = Self::build(source_dir, extensions, ignored_dirs); - fingerprint.save(cache_dir) + fingerprint.save(cache_dir, key) } } diff --git a/crates/capsule-core/src/wasm/compiler/javascript.rs b/crates/capsule-core/src/wasm/compiler/javascript.rs index c4874d7..7d46f24 100644 --- a/crates/capsule-core/src/wasm/compiler/javascript.rs +++ b/crates/capsule-core/src/wasm/compiler/javascript.rs @@ -125,6 +125,7 @@ impl JavascriptWasmCompiler { &self.output_wasm, &["js", "ts", "toml"], &["node_modules", "dist"], + Some("js"), ) { if export && let Some(file_stem) = self.source_path.file_stem() { let export_path = source_dir.join(file_stem).with_extension("wasm"); @@ -300,6 +301,7 @@ export {{ incomingHandler }}; source_dir, &["js", "ts", "toml"], &["node_modules", "dist"], + Some("js"), ); if export && let Some(file_stem) = self.source_path.file_stem() { diff --git a/crates/capsule-core/src/wasm/compiler/python.rs b/crates/capsule-core/src/wasm/compiler/python.rs index 12a9f6a..9965236 100644 --- a/crates/capsule-core/src/wasm/compiler/python.rs +++ b/crates/capsule-core/src/wasm/compiler/python.rs @@ -105,6 +105,7 @@ impl PythonWasmCompiler { &self.output_wasm, &["py", "toml"], &["__pycache__"], + Some("python"), ) { if export && let Some(file_stem) = self.source_path.file_stem() { let export_path = source_dir.join(file_stem).with_extension("wasm"); @@ -195,6 +196,7 @@ from capsule.app import TaskRunner, exports source_dir, &["py", "toml"], &["__pycache__"], + Some("python"), ); if export && let Some(file_stem) = self.source_path.file_stem() { diff --git a/crates/capsule-core/src/wasm/utilities/cache.rs b/crates/capsule-core/src/wasm/utilities/cache.rs index ec8abde..cf0c863 100644 --- a/crates/capsule-core/src/wasm/utilities/cache.rs +++ b/crates/capsule-core/src/wasm/utilities/cache.rs @@ -51,8 +51,8 @@ pub fn generate_wasm_filename(source_path: &Path) -> String { .unwrap_or("capsule"); let path_str = source_path.to_string_lossy(); - let hash = blake3::hash(path_str.as_bytes()).to_hex(); - let hash_str = hash.as_str(); + let hashdata = blake3::hash(path_str.as_bytes()).to_hex(); + let hash_str = hashdata.as_str(); let short_hash = &hash_str[..8]; format!("{stem}_{short_hash}.wasm") diff --git a/crates/capsule-sdk/javascript/src/polyfills/fs.ts b/crates/capsule-sdk/javascript/src/polyfills/fs.ts index 1dfe0fd..263a5b2 100644 --- a/crates/capsule-sdk/javascript/src/polyfills/fs.ts +++ b/crates/capsule-sdk/javascript/src/polyfills/fs.ts @@ -72,9 +72,13 @@ function getPreopenedDirs(): PreopenedDir[] { } function normalizePath(path: string): string { - if (path.startsWith('./')) return path.slice(2); - if (path.startsWith('/')) return path.slice(1); - return path; + let p = path; + + if (p.startsWith('./')) p = p.slice(2); + else if (p.startsWith('/')) p = p.slice(1); + if (p.length > 1) p = p.replace(/\/+$/, ''); + + return p; } /** @@ -90,6 +94,37 @@ function getEffectivePath(path: string): string { return cwd.replace(/\/+$/, '') + '/' + path; } +function resolveNormalizedPath(normalizedPath: string): { dir: Descriptor; relativePath: string } | null { + const preopens = getPreopenedDirs(); + if (preopens.length === 0) return null; + + const sorted = [...preopens].sort((a, b) => b.guestPath.length - a.guestPath.length); + + let catchAll: { dir: Descriptor; relativePath: string } | null = null; + + for (const { descriptor, guestPath } of sorted) { + const normalizedGuest = normalizePath(guestPath); + + if (normalizedGuest === '.' || normalizedGuest === '') { + if (!catchAll) catchAll = { dir: descriptor, relativePath: normalizedPath }; + continue; + } + + const guestPrefix = normalizedGuest.endsWith('/') ? normalizedGuest : normalizedGuest + '/'; + + if (normalizedPath.startsWith(guestPrefix)) { + const relativePath = normalizedPath.slice(guestPrefix.length); + return { dir: descriptor, relativePath: relativePath || '.' }; + } + + if (normalizedPath === normalizedGuest) { + return { dir: descriptor, relativePath: '.' }; + } + } + + return catchAll ?? { dir: preopens[0].descriptor, relativePath: normalizedPath }; +} + function resolvePath(path: string): { dir: Descriptor; relativePath: string } | null { const preopens = getPreopenedDirs(); if (preopens.length === 0) return null; @@ -473,7 +508,7 @@ export function mkdirSync(path: string, options?: MkdirOptions): void { const parts = normalized.split('/').filter(Boolean); for (let i = 1; i <= parts.length; i++) { const partial = parts.slice(0, i).join('/'); - const resolved = resolvePath(partial); + const resolved = resolveNormalizedPath(partial); if (!resolved) continue; try { resolved.dir.createDirectoryAt(resolved.relativePath); } catch { /* already exists */ } } @@ -504,6 +539,33 @@ export function rmdirSync(path: string, _options?: RmdirOptions): void { /** * Remove a file or directory synchronously. */ +function removeRecursiveSync(path: string): void { + const resolved = resolvePath(path); + if (!resolved) throw enoent(path); + + const fd = resolved.dir.openAt( + { symlinkFollow: false }, + resolved.relativePath, + { directory: true }, + { read: true, mutateDirectory: true } + ); + + const stream = fd.readDirectory(); + let entry: DirectoryEntry | null | undefined; + while ((entry = stream.readDirectoryEntry()) && entry) { + if (!entry.name) continue; + const childPath = path.replace(/\/$/, '') + '/' + entry.name; + if (entry.type === 'directory') { + removeRecursiveSync(childPath); + } else { + const childResolved = resolvePath(childPath); + if (childResolved) childResolved.dir.unlinkFileAt(childResolved.relativePath); + } + } + + resolved.dir.removeDirectoryAt(resolved.relativePath); +} + export function rmSync(path: string, options?: RmOptions): void { const resolved = resolvePath(path); if (!resolved) { @@ -521,7 +583,7 @@ export function rmSync(path: string, options?: RmOptions): void { { code: 'EISDIR' } ); } - resolved.dir.removeDirectoryAt(resolved.relativePath); + removeRecursiveSync(path); } else { resolved.dir.unlinkFileAt(resolved.relativePath); } @@ -729,7 +791,7 @@ export async function mkdir(path: string, options?: MkdirOptions): Promise const parts = normalized.split('/').filter(Boolean); for (let i = 1; i <= parts.length; i++) { const partial = parts.slice(0, i).join('/'); - const resolved = resolvePath(partial); + const resolved = resolveNormalizedPath(partial); if (!resolved) continue; try { resolved.dir.createDirectoryAt(resolved.relativePath); @@ -780,6 +842,7 @@ export interface CpOptions { */ export async function cp(src: string, dest: string, options?: CpOptions): Promise { const kind = await statPath(src); + if (kind === 'notfound') { throw new Error(`ENOENT: no such file or directory '${src}'`); } @@ -787,7 +850,14 @@ export async function cp(src: string, dest: string, options?: CpOptions): Promis if (!options?.recursive) { throw new Error(`EISDIR: illegal operation on a directory '${src}' (use { recursive: true })`); } - await copyDirRecursive(src, dest); + + const destKind = await statPath(dest); + const srcName = src.replace(/\/$/, '').split('/').pop()!; + const effectiveDest = destKind === 'directory' + ? dest.replace(/\/$/, '') + '/' + srcName + : dest; + + await copyDirRecursive(src, effectiveDest); } else { await copyFile(src, dest); }