Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

add subfs utilities to main backend

Adds extractSubfsUris() to find all subfs references in a directory tree,
tracking their mount paths for proper blob path resolution.

Also adds replaceDirectoryWithSubfs() and findLargeDirectories() to
support automatic manifest splitting when sites exceed size limits.

Changed files
+145
src
+145
src/lib/wisp-utils.ts
··· 80 80 81 81 // Remove any base folder name from the path 82 82 const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 83 + 84 + // Skip files in .git directories 85 + if (normalizedPath.startsWith('.git/') || normalizedPath === '.git') { 86 + continue; 87 + } 88 + 83 89 const parts = normalizedPath.split('/'); 84 90 85 91 if (parts.length === 1) { ··· 296 302 const subMap = extractBlobMap(entry.node as Directory, fullPath); 297 303 subMap.forEach((value, key) => blobMap.set(key, value)); 298 304 } 305 + // Skip subfs nodes - they don't contain blobs in the main tree 299 306 } 300 307 301 308 return blobMap; 302 309 } 310 + 311 + /** 312 + * Extract all subfs URIs from a directory tree with their mount paths 313 + */ 314 + export function extractSubfsUris( 315 + directory: Directory, 316 + currentPath: string = '' 317 + ): Array<{ uri: string; path: string }> { 318 + const uris: Array<{ uri: string; path: string }> = []; 319 + 320 + for (const entry of directory.entries) { 321 + const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 322 + 323 + if ('type' in entry.node) { 324 + if (entry.node.type === 'subfs') { 325 + // Subfs node with subject URI 326 + const subfsNode = entry.node as any; 327 + if (subfsNode.subject) { 328 + uris.push({ uri: subfsNode.subject, path: fullPath }); 329 + } 330 + } else if (entry.node.type === 'directory') { 331 + // Recursively search subdirectories 332 + const subUris = extractSubfsUris(entry.node as Directory, fullPath); 333 + uris.push(...subUris); 334 + } 335 + } 336 + } 337 + 338 + return uris; 339 + } 340 + 341 + /** 342 + * Estimate the JSON size of a directory tree 343 + */ 344 + export function estimateDirectorySize(directory: Directory): number { 345 + return JSON.stringify(directory).length; 346 + } 347 + 348 + /** 349 + * Count files in a directory tree 350 + */ 351 + export function countFilesInDirectory(directory: Directory): number { 352 + let count = 0; 353 + for (const entry of directory.entries) { 354 + if ('type' in entry.node && entry.node.type === 'file') { 355 + count++; 356 + } else if ('type' in entry.node && entry.node.type === 'directory') { 357 + count += countFilesInDirectory(entry.node as Directory); 358 + } 359 + } 360 + return count; 361 + } 362 + 363 + /** 364 + * Find all directories in a tree with their paths and sizes 365 + */ 366 + export function findLargeDirectories(directory: Directory, currentPath: string = ''): Array<{ 367 + path: string; 368 + directory: Directory; 369 + size: number; 370 + fileCount: number; 371 + }> { 372 + const result: Array<{ path: string; directory: Directory; size: number; fileCount: number }> = []; 373 + 374 + for (const entry of directory.entries) { 375 + if ('type' in entry.node && entry.node.type === 'directory') { 376 + const dirPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 377 + const dir = entry.node as Directory; 378 + const size = estimateDirectorySize(dir); 379 + const fileCount = countFilesInDirectory(dir); 380 + 381 + result.push({ path: dirPath, directory: dir, size, fileCount }); 382 + 383 + // Recursively find subdirectories 384 + const subdirs = findLargeDirectories(dir, dirPath); 385 + result.push(...subdirs); 386 + } 387 + } 388 + 389 + return result; 390 + } 391 + 392 + /** 393 + * Replace a directory with a subfs node in the tree 394 + */ 395 + export function replaceDirectoryWithSubfs( 396 + directory: Directory, 397 + targetPath: string, 398 + subfsUri: string 399 + ): Directory { 400 + const pathParts = targetPath.split('/'); 401 + const targetName = pathParts[pathParts.length - 1]; 402 + const parentPath = pathParts.slice(0, -1).join('/'); 403 + 404 + // If this is a root-level directory 405 + if (pathParts.length === 1) { 406 + const newEntries = directory.entries.map(entry => { 407 + if (entry.name === targetName && 'type' in entry.node && entry.node.type === 'directory') { 408 + return { 409 + name: entry.name, 410 + node: { 411 + $type: 'place.wisp.fs#subfs' as const, 412 + type: 'subfs' as const, 413 + subject: subfsUri 414 + } 415 + }; 416 + } 417 + return entry; 418 + }); 419 + 420 + return { 421 + $type: 'place.wisp.fs#directory' as const, 422 + type: 'directory' as const, 423 + entries: newEntries 424 + }; 425 + } 426 + 427 + // Recursively navigate to parent directory 428 + const newEntries = directory.entries.map(entry => { 429 + if ('type' in entry.node && entry.node.type === 'directory') { 430 + const entryPath = entry.name; 431 + if (parentPath.startsWith(entryPath) || parentPath === entry.name) { 432 + const remainingPath = pathParts.slice(1).join('/'); 433 + return { 434 + name: entry.name, 435 + node: replaceDirectoryWithSubfs(entry.node as Directory, remainingPath, subfsUri) 436 + }; 437 + } 438 + } 439 + return entry; 440 + }); 441 + 442 + return { 443 + $type: 'place.wisp.fs#directory' as const, 444 + type: 'directory' as const, 445 + entries: newEntries 446 + }; 447 + }