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

Compare changes

Choose any two refs to compare.

Changed files
+371 -30
apps
hosting-service
src
main-app
src
lib
routes
docs
src
assets
components
content
docs
packages
@wisp
fs-utils
observability
+3 -1
apps/hosting-service/src/index.ts
··· 4 4 import { createLogger, initializeGrafanaExporters } from '@wisp/observability'; 5 5 import { mkdirSync, existsSync } from 'fs'; 6 6 import { backfillCache } from './lib/backfill'; 7 - import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db'; 7 + import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode, closeDatabase } from './lib/db'; 8 8 9 9 // Initialize Grafana exporters if configured 10 10 initializeGrafanaExporters({ ··· 94 94 console.log('\n🛑 Shutting down...'); 95 95 firehose.stop(); 96 96 stopDomainCacheCleanup(); 97 + await closeDatabase(); 97 98 server.close(); 98 99 process.exit(0); 99 100 }); ··· 102 103 console.log('\n🛑 Shutting down...'); 103 104 firehose.stop(); 104 105 stopDomainCacheCleanup(); 106 + await closeDatabase(); 105 107 server.close(); 106 108 process.exit(0); 107 109 });
+32 -1
apps/hosting-service/src/lib/db.ts
··· 183 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 184 } 185 185 186 + // Track active locks for cleanup on shutdown 187 + const activeLocks = new Set<string>(); 188 + 186 189 /** 187 190 * Acquire a distributed lock using PostgreSQL advisory locks 188 191 * Returns true if lock was acquired, false if already held by another instance ··· 193 196 194 197 try { 195 198 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 196 - return result[0]?.acquired === true; 199 + const acquired = result[0]?.acquired === true; 200 + if (acquired) { 201 + activeLocks.add(key); 202 + } 203 + return acquired; 197 204 } catch (err) { 198 205 console.error('Failed to acquire lock', { key, error: err }); 199 206 return false; ··· 208 215 209 216 try { 210 217 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 218 + activeLocks.delete(key); 211 219 } catch (err) { 212 220 console.error('Failed to release lock', { key, error: err }); 221 + // Still remove from tracking even if unlock fails 222 + activeLocks.delete(key); 223 + } 224 + } 225 + 226 + /** 227 + * Close all database connections 228 + * Call this during graceful shutdown 229 + */ 230 + export async function closeDatabase(): Promise<void> { 231 + try { 232 + // Release all active advisory locks before closing connections 233 + if (activeLocks.size > 0) { 234 + console.log(`[DB] Releasing ${activeLocks.size} active advisory locks before shutdown`); 235 + for (const key of activeLocks) { 236 + await releaseLock(key); 237 + } 238 + } 239 + 240 + await sql.end({ timeout: 5 }); 241 + console.log('[DB] Database connections closed'); 242 + } catch (err) { 243 + console.error('[DB] Error closing database connections:', err); 213 244 } 214 245 } 215 246
+16 -1
apps/main-app/src/index.ts
··· 12 12 cleanupExpiredSessions, 13 13 rotateKeysIfNeeded 14 14 } from './lib/oauth-client' 15 - import { getCookieSecret } from './lib/db' 15 + import { getCookieSecret, closeDatabase } from './lib/db' 16 16 import { authRoutes } from './routes/auth' 17 17 import { wispRoutes } from './routes/wisp' 18 18 import { domainRoutes } from './routes/domain' ··· 205 205 console.log( 206 206 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 207 207 ) 208 + 209 + // Graceful shutdown 210 + process.on('SIGINT', async () => { 211 + console.log('\n🛑 Shutting down...') 212 + dnsVerifier.stop() 213 + await closeDatabase() 214 + process.exit(0) 215 + }) 216 + 217 + process.on('SIGTERM', async () => { 218 + console.log('\n🛑 Shutting down...') 219 + dnsVerifier.stop() 220 + await closeDatabase() 221 + process.exit(0) 222 + })
+13
apps/main-app/src/lib/db.ts
··· 526 526 console.log('[CookieSecret] Generated new cookie signing secret'); 527 527 return secret; 528 528 }; 529 + 530 + /** 531 + * Close database connection 532 + * Call this during graceful shutdown 533 + */ 534 + export const closeDatabase = async (): Promise<void> => { 535 + try { 536 + await db.end(); 537 + console.log('[DB] Database connection closed'); 538 + } catch (err) { 539 + console.error('[DB] Error closing database connection:', err); 540 + } 541 + };
+10 -16
apps/main-app/src/routes/wisp.ts
··· 39 39 40 40 const logger = createLogger('main-app') 41 41 42 - function isValidSiteName(siteName: string): boolean { 42 + export function isValidSiteName(siteName: string): boolean { 43 43 if (!siteName || typeof siteName !== 'string') return false; 44 44 45 45 // Length check (AT Protocol rkey limit) ··· 183 183 continue; 184 184 } 185 185 186 - console.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes'); 186 + // Use webkitRelativePath when available (directory uploads), fallback to name for regular file uploads 187 + const webkitPath = 'webkitRelativePath' in file ? String(file.webkitRelativePath) : ''; 188 + const filePath = webkitPath || file.name; 189 + 187 190 updateJobProgress(jobId, { 188 191 filesProcessed: i + 1, 189 - currentFile: file.name 192 + currentFile: filePath 190 193 }); 191 194 192 195 // Skip files that match ignore patterns 193 - const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 196 + const normalizedPath = filePath.replace(/^[^\/]*\//, ''); 194 197 195 198 if (shouldIgnore(ignoreMatcher, normalizedPath)) { 196 - console.log(`Skipping ignored file: ${file.name}`); 197 199 skippedFiles.push({ 198 - name: file.name, 200 + name: filePath, 199 201 reason: 'matched ignore pattern' 200 202 }); 201 203 continue; ··· 205 207 const maxSize = MAX_FILE_SIZE; 206 208 if (file.size > maxSize) { 207 209 skippedFiles.push({ 208 - name: file.name, 210 + name: filePath, 209 211 reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)` 210 212 }); 211 213 continue; ··· 238 240 // Text files: compress AND base64 encode 239 241 finalContent = Buffer.from(compressedContent.toString('base64'), 'binary'); 240 242 base64Encoded = true; 241 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 242 - console.log(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 243 - logger.info(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 244 243 } else { 245 244 // Audio files: just compress, no base64 246 245 finalContent = compressedContent; 247 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 248 - console.log(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 249 - logger.info(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 250 246 } 251 247 } else { 252 248 // Binary files: upload directly 253 249 finalContent = originalContent; 254 - console.log(`Uploading ${file.name} directly: ${originalContent.length} bytes (no compression)`); 255 - logger.info(`Uploading ${file.name} directly: ${originalContent.length} bytes (binary)`); 256 250 } 257 251 258 252 uploadedFiles.push({ 259 - name: file.name, 253 + name: filePath, 260 254 content: finalContent, 261 255 mimeType: originalMimeType, 262 256 size: finalContent.length,
+9 -2
bun.lock
··· 11 11 "elysia": "^1.4.18", 12 12 "tailwindcss": "^4.1.17", 13 13 }, 14 + "devDependencies": { 15 + "@types/bun": "^1.3.5", 16 + }, 14 17 }, 15 18 "apps/hosting-service": { 16 19 "name": "wisp-hosting-service", ··· 583 586 584 587 "@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="], 585 588 586 - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 589 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 587 590 588 591 "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 589 592 ··· 1267 1270 1268 1271 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1269 1272 1270 - "@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 1273 + "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1271 1274 1272 1275 "@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1273 1276 ··· 1298 1301 "uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1299 1302 1300 1303 "wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1304 + 1305 + "wisp-hosting-service/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 1301 1306 1302 1307 "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="], 1303 1308 ··· 1400 1405 "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], 1401 1406 1402 1407 "wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1408 + 1409 + "wisp-hosting-service/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 1403 1410 } 1404 1411 }
+3 -1
docs/astro.config.mjs
··· 7 7 integrations: [ 8 8 starlight({ 9 9 title: 'Wisp.place Docs', 10 - social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/tangled-org/wisp.place' }], 10 + components: { 11 + SocialIcons: './src/components/SocialIcons.astro', 12 + }, 11 13 sidebar: [ 12 14 { 13 15 label: 'Getting Started',
+9
docs/src/assets/tangled-icon.svg
··· 1 + <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg1" width="25" height="25" viewBox="0 0 25 25" sodipodi:docname="tangled_dolly_silhouette.png"> 2 + <defs id="defs1"/> 3 + <sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1"> 4 + <inkscape:page x="0" y="0" width="25" height="25" id="page2" margin="0" bleed="0"/> 5 + </sodipodi:namedview> 6 + <g inkscape:groupmode="layer" inkscape:label="Image" id="g1"> 7 + <path style="fill:#000000;stroke-width:1.12248" d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" id="path1"/> 8 + </g> 9 + </svg>
+26
docs/src/components/SocialIcons.astro
··· 1 + --- 2 + // Custom social icons component to use the Tangled icon 3 + --- 4 + 5 + <div class="sl-flex"> 6 + <a 7 + href="https://tangled.org/nekomimi.pet/wisp.place-monorepo" 8 + rel="me" 9 + class="sl-flex" 10 + aria-label="Tangled" 11 + > 12 + <svg 13 + xmlns="http://www.w3.org/2000/svg" 14 + viewBox="0 0 25 25" 15 + width="16" 16 + height="16" 17 + aria-hidden="true" 18 + focusable="false" 19 + > 20 + <path 21 + style="fill:currentColor;stroke-width:1.12248" 22 + d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" 23 + ></path> 24 + </svg> 25 + </a> 26 + </div>
-5
docs/src/content/docs/cli.md
··· 71 71 72 72 engine: 'nixery' 73 73 74 - clone: 75 - skip: false 76 - depth: 1 77 - submodules: false 78 - 79 74 dependencies: 80 75 nixpkgs: 81 76 - nodejs
+4 -1
package.json
··· 30 30 "@parcel/watcher", 31 31 "bun", 32 32 "esbuild" 33 - ] 33 + ], 34 + "devDependencies": { 35 + "@types/bun": "^1.3.5" 36 + } 34 37 }
+244
packages/@wisp/fs-utils/src/tree.test.ts
··· 1 + import { describe, test, expect } from 'bun:test' 2 + import { processUploadedFiles, type UploadedFile } from './tree' 3 + 4 + describe('processUploadedFiles', () => { 5 + test('should preserve nested directory structure', () => { 6 + const files: UploadedFile[] = [ 7 + { 8 + name: 'mysite/index.html', 9 + content: Buffer.from('<html>'), 10 + mimeType: 'text/html', 11 + size: 6 12 + }, 13 + { 14 + name: 'mysite/_astro/main.js', 15 + content: Buffer.from('console.log()'), 16 + mimeType: 'application/javascript', 17 + size: 13 18 + }, 19 + { 20 + name: 'mysite/_astro/styles.css', 21 + content: Buffer.from('body {}'), 22 + mimeType: 'text/css', 23 + size: 7 24 + }, 25 + { 26 + name: 'mysite/images/logo.png', 27 + content: Buffer.from([0x89, 0x50, 0x4e, 0x47]), 28 + mimeType: 'image/png', 29 + size: 4 30 + } 31 + ] 32 + 33 + const result = processUploadedFiles(files) 34 + 35 + expect(result.fileCount).toBe(4) 36 + expect(result.directory.entries).toHaveLength(3) // index.html, _astro/, images/ 37 + 38 + // Check _astro directory exists 39 + const astroEntry = result.directory.entries.find(e => e.name === '_astro') 40 + expect(astroEntry).toBeTruthy() 41 + expect('type' in astroEntry!.node && astroEntry!.node.type).toBe('directory') 42 + 43 + if ('entries' in astroEntry!.node) { 44 + const astroDir = astroEntry!.node 45 + expect(astroDir.entries).toHaveLength(2) // main.js, styles.css 46 + expect(astroDir.entries.find(e => e.name === 'main.js')).toBeTruthy() 47 + expect(astroDir.entries.find(e => e.name === 'styles.css')).toBeTruthy() 48 + } 49 + 50 + // Check images directory exists 51 + const imagesEntry = result.directory.entries.find(e => e.name === 'images') 52 + expect(imagesEntry).toBeTruthy() 53 + expect('type' in imagesEntry!.node && imagesEntry!.node.type).toBe('directory') 54 + 55 + if ('entries' in imagesEntry!.node) { 56 + const imagesDir = imagesEntry!.node 57 + expect(imagesDir.entries).toHaveLength(1) // logo.png 58 + expect(imagesDir.entries.find(e => e.name === 'logo.png')).toBeTruthy() 59 + } 60 + }) 61 + 62 + test('should handle deeply nested directories', () => { 63 + const files: UploadedFile[] = [ 64 + { 65 + name: 'site/a/b/c/d/deep.txt', 66 + content: Buffer.from('deep'), 67 + mimeType: 'text/plain', 68 + size: 4 69 + } 70 + ] 71 + 72 + const result = processUploadedFiles(files) 73 + 74 + expect(result.fileCount).toBe(1) 75 + 76 + // Navigate through nested structure 77 + const aEntry = result.directory.entries.find(e => e.name === 'a') 78 + expect(aEntry).toBeTruthy() 79 + expect('type' in aEntry!.node && aEntry!.node.type).toBe('directory') 80 + 81 + if ('entries' in aEntry!.node) { 82 + const bEntry = aEntry!.node.entries.find(e => e.name === 'b') 83 + expect(bEntry).toBeTruthy() 84 + expect('type' in bEntry!.node && bEntry!.node.type).toBe('directory') 85 + 86 + if ('entries' in bEntry!.node) { 87 + const cEntry = bEntry!.node.entries.find(e => e.name === 'c') 88 + expect(cEntry).toBeTruthy() 89 + expect('type' in cEntry!.node && cEntry!.node.type).toBe('directory') 90 + 91 + if ('entries' in cEntry!.node) { 92 + const dEntry = cEntry!.node.entries.find(e => e.name === 'd') 93 + expect(dEntry).toBeTruthy() 94 + expect('type' in dEntry!.node && dEntry!.node.type).toBe('directory') 95 + 96 + if ('entries' in dEntry!.node) { 97 + const fileEntry = dEntry!.node.entries.find(e => e.name === 'deep.txt') 98 + expect(fileEntry).toBeTruthy() 99 + expect('type' in fileEntry!.node && fileEntry!.node.type).toBe('file') 100 + } 101 + } 102 + } 103 + } 104 + }) 105 + 106 + test('should handle files at root level', () => { 107 + const files: UploadedFile[] = [ 108 + { 109 + name: 'mysite/index.html', 110 + content: Buffer.from('<html>'), 111 + mimeType: 'text/html', 112 + size: 6 113 + }, 114 + { 115 + name: 'mysite/robots.txt', 116 + content: Buffer.from('User-agent: *'), 117 + mimeType: 'text/plain', 118 + size: 13 119 + } 120 + ] 121 + 122 + const result = processUploadedFiles(files) 123 + 124 + expect(result.fileCount).toBe(2) 125 + expect(result.directory.entries).toHaveLength(2) 126 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 127 + expect(result.directory.entries.find(e => e.name === 'robots.txt')).toBeTruthy() 128 + }) 129 + 130 + test('should skip .git directories', () => { 131 + const files: UploadedFile[] = [ 132 + { 133 + name: 'mysite/index.html', 134 + content: Buffer.from('<html>'), 135 + mimeType: 'text/html', 136 + size: 6 137 + }, 138 + { 139 + name: 'mysite/.git/config', 140 + content: Buffer.from('[core]'), 141 + mimeType: 'text/plain', 142 + size: 6 143 + }, 144 + { 145 + name: 'mysite/.gitignore', 146 + content: Buffer.from('node_modules'), 147 + mimeType: 'text/plain', 148 + size: 12 149 + } 150 + ] 151 + 152 + const result = processUploadedFiles(files) 153 + 154 + expect(result.fileCount).toBe(2) // Only index.html and .gitignore 155 + expect(result.directory.entries).toHaveLength(2) 156 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 157 + expect(result.directory.entries.find(e => e.name === '.gitignore')).toBeTruthy() 158 + expect(result.directory.entries.find(e => e.name === '.git')).toBeFalsy() 159 + }) 160 + 161 + test('should handle mixed root and nested files', () => { 162 + const files: UploadedFile[] = [ 163 + { 164 + name: 'mysite/index.html', 165 + content: Buffer.from('<html>'), 166 + mimeType: 'text/html', 167 + size: 6 168 + }, 169 + { 170 + name: 'mysite/about/index.html', 171 + content: Buffer.from('<html>'), 172 + mimeType: 'text/html', 173 + size: 6 174 + }, 175 + { 176 + name: 'mysite/about/team.html', 177 + content: Buffer.from('<html>'), 178 + mimeType: 'text/html', 179 + size: 6 180 + }, 181 + { 182 + name: 'mysite/robots.txt', 183 + content: Buffer.from('User-agent: *'), 184 + mimeType: 'text/plain', 185 + size: 13 186 + } 187 + ] 188 + 189 + const result = processUploadedFiles(files) 190 + 191 + expect(result.fileCount).toBe(4) 192 + expect(result.directory.entries).toHaveLength(3) // index.html, about/, robots.txt 193 + 194 + const aboutEntry = result.directory.entries.find(e => e.name === 'about') 195 + expect(aboutEntry).toBeTruthy() 196 + expect('type' in aboutEntry!.node && aboutEntry!.node.type).toBe('directory') 197 + 198 + if ('entries' in aboutEntry!.node) { 199 + const aboutDir = aboutEntry!.node 200 + expect(aboutDir.entries).toHaveLength(2) // index.html, team.html 201 + expect(aboutDir.entries.find(e => e.name === 'index.html')).toBeTruthy() 202 + expect(aboutDir.entries.find(e => e.name === 'team.html')).toBeTruthy() 203 + } 204 + }) 205 + 206 + test('should handle empty file array', () => { 207 + const files: UploadedFile[] = [] 208 + 209 + const result = processUploadedFiles(files) 210 + 211 + expect(result.fileCount).toBe(0) 212 + expect(result.directory.entries).toHaveLength(0) 213 + }) 214 + 215 + test('should strip base folder name from paths', () => { 216 + // This tests the behavior where file.name includes the base folder 217 + // e.g., "mysite/index.html" should become "index.html" at root 218 + const files: UploadedFile[] = [ 219 + { 220 + name: 'build-output/index.html', 221 + content: Buffer.from('<html>'), 222 + mimeType: 'text/html', 223 + size: 6 224 + }, 225 + { 226 + name: 'build-output/assets/main.js', 227 + content: Buffer.from('console.log()'), 228 + mimeType: 'application/javascript', 229 + size: 13 230 + } 231 + ] 232 + 233 + const result = processUploadedFiles(files) 234 + 235 + expect(result.fileCount).toBe(2) 236 + 237 + // Should have index.html at root and assets/ directory 238 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 239 + expect(result.directory.entries.find(e => e.name === 'assets')).toBeTruthy() 240 + 241 + // Should NOT have 'build-output' directory 242 + expect(result.directory.entries.find(e => e.name === 'build-output')).toBeFalsy() 243 + }) 244 + })
+1 -1
packages/@wisp/observability/README.md
··· 214 214 215 215 ## License 216 216 217 - Private 217 + MIT
+1 -1
tsconfig.json
··· 33 33 // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 34 // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 35 "types": [ 36 - "bun-types" 36 + "bun" 37 37 ] /* Specify type package names to be included without being referenced in a source file. */, 38 38 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 39 // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */