Skip to content
Merged
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
23 changes: 15 additions & 8 deletions crates/capsule-core/src/config/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/capsule-core/src/wasm/compiler/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions crates/capsule-core/src/wasm/compiler/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions crates/capsule-core/src/wasm/utilities/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
84 changes: 77 additions & 7 deletions crates/capsule-sdk/javascript/src/polyfills/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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 */ }
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -729,7 +791,7 @@ export async function mkdir(path: string, options?: MkdirOptions): Promise<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);
Expand Down Expand Up @@ -780,14 +842,22 @@ export interface CpOptions {
*/
export async function cp(src: string, dest: string, options?: CpOptions): Promise<void> {
const kind = await statPath(src);

if (kind === 'notfound') {
throw new Error(`ENOENT: no such file or directory '${src}'`);
}
if (kind === 'directory') {
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);
}
Expand Down
Loading