@shipstatic/drop 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 shipstatic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  **Headless file processing toolkit for Ship SDK deployments**
4
4
 
5
- A focused React hook for preparing files for deployment with [@shipstatic/ship](https://github.com/shipstatic/ship). Handles ZIP extraction, MD5 calculation, path normalization, and validation - everything needed before calling `ship.deploy()`.
5
+ A focused React hook for preparing files for deployment with [@shipstatic/ship](https://github.com/shipstatic/ship). Handles ZIP extraction, path normalization, and validation - everything needed before calling `ship.deploy()`.
6
+
7
+ **Note:** MD5 calculation is handled by Ship SDK during deployment. Drop focuses on file processing and UI state management.
6
8
 
7
9
  ## Why Headless?
8
10
 
@@ -13,18 +15,17 @@ This package provides **zero UI components**. You build the dropzone that fits y
13
15
  3. **Smaller bundle** - No React components, no extra dependencies (~14KB saved vs generic libraries)
14
16
  4. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
15
17
 
16
- The package focuses on what's hard (ZIP extraction, MD5 calculation, validation) and leaves what's easy (UI) to you.
18
+ The package focuses on what's hard (ZIP extraction, folder structure preservation) and leaves what's easy (UI) to you.
17
19
 
18
20
  ## Features
19
21
 
20
22
  - 🎯 **Headless Architecture** - Just the hook, no UI opinions
21
23
  - 📦 **ZIP Support** - Automatic ZIP file extraction and processing
22
- - ✅ **Validation** - Client-side file size, count, and total size validation
23
- - 🗑️ **Junk Filtering** - Automatically filters `.DS_Store`, `Thumbs.db`, etc.
24
- - 🔍 **MD5 Hashing** - Calculates MD5 checksums for all files
24
+ - ✅ **Validation** - Client-side file size, count, and total size validation (powered by Ship SDK)
25
+ - 🗑️ **Junk Filtering** - Automatically filters `.DS_Store`, `Thumbs.db`, etc. (powered by Ship SDK)
25
26
  - 🔒 **Path Sanitization** - Defense-in-depth protection against directory traversal attacks
26
27
  - 📁 **Folder Structure Preservation** - Respects `webkitRelativePath` for proper deployment paths
27
- - 🚀 **SDK Agnostic** - Works with any upload SDK (Ship, AWS S3, etc.)
28
+ - 🚀 **Focused Scope** - File processing and UI state only. MD5 calculation and deployment handled by Ship SDK
28
29
 
29
30
  ## Installation
30
31
 
@@ -40,21 +41,18 @@ pnpm add @shipstatic/drop
40
41
  import { useDrop } from '@shipstatic/drop';
41
42
  import Ship from '@shipstatic/ship';
42
43
 
43
- function MyUploader() {
44
- const ship = new Ship({ apiUrl: '...' });
45
-
46
- // Get validation config from Ship SDK
47
- const config = await ship.getConfig();
44
+ const ship = new Ship({ deployToken: 'token-xxxx' });
48
45
 
46
+ function MyUploader() {
49
47
  const drop = useDrop({
50
- config // Pass SDK config directly
48
+ ship // Pass Ship instance - Drop uses ship.getConfig() for validation
51
49
  });
52
50
 
53
51
  const handleUpload = async () => {
54
52
  const validFiles = drop.getValidFiles();
55
53
 
56
54
  // ProcessedFile extends StaticFile - no conversion needed!
57
- await ship.deployments.create({ files: validFiles });
55
+ await ship.deployments.create(validFiles.map(f => f.file));
58
56
  };
59
57
 
60
58
  return (
@@ -87,6 +85,31 @@ function MyUploader() {
87
85
  }
88
86
  ```
89
87
 
88
+ ### ⚠️ Configuration Architecture
89
+
90
+ **Drop uses Ship's validation config automatically:**
91
+
92
+ Drop accepts a `Ship` instance and uses `ship.getConfig()` internally. This ensures:
93
+ - ✅ **Single source of truth** - Validation config comes from Ship SDK
94
+ - ✅ **Always in sync** - Client validation matches server limits
95
+ - ✅ **No manual config fetching** - Drop handles it internally
96
+ - ✅ **Simpler API** - Just pass `ship` instance
97
+
98
+ ```tsx
99
+ // Drop fetches config from Ship SDK automatically
100
+ const drop = useDrop({ ship });
101
+
102
+ // Behind the scenes:
103
+ // 1. Ship SDK fetches /config on initialization
104
+ // 2. Drop calls ship.getConfig() when validating
105
+ // 3. Validation always uses current server limits
106
+ ```
107
+
108
+ **Why this architecture:**
109
+ - Drop has NO validation rules of its own - it's a pure proxy
110
+ - Ship SDK is the single source of truth for validation
111
+ - Drop only provides what Ship doesn't have (ZIP, React state, folder structure)
112
+
90
113
  ## Building Your Drop Zone with Folder Support
91
114
 
92
115
  For production use, you'll want to support folder drag-and-drop using modern browser APIs. Here's a complete example:
@@ -243,8 +266,8 @@ Main hook for managing drop state.
243
266
 
244
267
  ```typescript
245
268
  interface DropOptions {
246
- /** Validation configuration (from ship.getConfig()) */
247
- config?: Partial<ValidationConfig>;
269
+ /** Ship SDK instance (required for validation) */
270
+ ship: Ship;
248
271
  /** Callback when validation fails */
249
272
  onValidationError?: (error: ClientError) => void;
250
273
  /** Callback when files are ready for upload */
@@ -266,13 +289,9 @@ interface DropReturn {
266
289
  isProcessing: boolean;
267
290
  /** Last validation error if any */
268
291
  validationError: ClientError | null;
269
- /** Whether all valid files have MD5 checksums calculated */
270
- hasChecksums: boolean;
271
292
 
272
293
  /** Process files from drop (resets and replaces existing files) */
273
294
  processFiles: (files: File[]) => Promise<void>;
274
- /** Remove a specific file */
275
- removeFile: (fileId: string) => void;
276
295
  /** Clear all files and reset state */
277
296
  clearAll: () => void;
278
297
  /** Get only valid files ready for upload */
@@ -286,19 +305,121 @@ interface DropReturn {
286
305
  }
287
306
  ```
288
307
 
308
+ ## Error Handling
309
+
310
+ ### Per-File Error Display
311
+
312
+ Each file in the `files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
313
+
314
+ ```tsx
315
+ function FileList({ drop }) {
316
+ return (
317
+ <div>
318
+ {drop.files.map(file => (
319
+ <div key={file.id}>
320
+ <span>{file.path}</span>
321
+
322
+ {/* Show status indicator */}
323
+ {file.status === 'ready' ? '✓' : '✗'}
324
+
325
+ {/* Show per-file error message */}
326
+ {file.status !== 'ready' && file.statusMessage && (
327
+ <span style={{ color: 'red' }}>
328
+ {file.statusMessage}
329
+ </span>
330
+ )}
331
+ </div>
332
+ ))}
333
+
334
+ {/* If validation fails, allow user to clear all and try again */}
335
+ {drop.validationError && (
336
+ <button onClick={drop.clearAll}>
337
+ Clear All & Try Again
338
+ </button>
339
+ )}
340
+ </div>
341
+ );
342
+ }
343
+ ```
344
+
345
+ **Common error statuses:**
346
+ - `validation_failed` - File failed validation (size, type, name, etc.)
347
+ - `processing_error` - MD5 calculation or processing failed
348
+ - `empty_file` - File is 0 bytes
349
+ - `ready` - File passed all validation and is ready for upload
350
+
351
+ ### Validation Error Summary
352
+
353
+ The `validationError` provides a summary when any files fail validation:
354
+
355
+ ```tsx
356
+ {drop.validationError && (
357
+ <div>
358
+ <p>{drop.validationError.error}</p>
359
+ <p>{drop.validationError.details}</p>
360
+ </div>
361
+ )}
362
+ ```
363
+
364
+ **Atomic Validation**: If ANY file fails validation, ALL files are marked as `validation_failed`. This ensures deployments are all-or-nothing for data integrity. The Ship SDK follows this same pattern server-side.
365
+
366
+ ### No Individual File Removal
367
+
368
+ The Drop package **intentionally does not support removing individual files**. Here's why:
369
+
370
+ **Reason:** Ship SDK uses **atomic validation** - if ANY file fails validation, ALL files are marked as `validation_failed`. This ensures deployments are all-or-nothing for data integrity.
371
+
372
+ **The Problem with Individual Removal:**
373
+ ```tsx
374
+ // User drops 5 files, 1 is too large
375
+ // Atomic validation: ALL 5 files marked as validation_failed
376
+
377
+ // If we allowed removing the large file:
378
+ drop.removeFile(largeFileId); // ❌ We don't support this!
379
+
380
+ // Would need to re-validate remaining 4 files
381
+ // Creates complexity and race conditions
382
+ ```
383
+
384
+ **The Simple Solution:**
385
+ Use `clearAll()` to reset and try again:
386
+
387
+ ```tsx
388
+ // If validation fails, show user which files failed
389
+ {drop.validationError && (
390
+ <div>
391
+ <p>Validation failed. Please fix the issues and try again:</p>
392
+ {drop.files.map(file => (
393
+ <div key={file.id}>
394
+ {file.path}: {file.statusMessage}
395
+ </div>
396
+ ))}
397
+ <button onClick={drop.clearAll}>Clear All & Try Again</button>
398
+ </div>
399
+ )}
400
+ ```
401
+
402
+ **Benefits:**
403
+ - ✅ No race conditions or stale validation state
404
+ - ✅ Simpler mental model (atomic = all-or-nothing)
405
+ - ✅ Aligns with Ship SDK's validation philosophy
406
+ - ✅ Clear UX: fix the problem, then re-drop
407
+
289
408
  ## Types
290
409
 
291
410
  ```typescript
292
411
  /**
293
412
  * ProcessedFile extends StaticFile from @shipstatic/types
294
413
  * This means it can be passed directly to ship.deployments.create()
414
+ *
415
+ * Note: md5 is intentionally undefined - Ship SDK calculates it during deployment
295
416
  */
296
417
  interface ProcessedFile extends StaticFile {
297
418
  // StaticFile properties (SDK compatibility)
298
419
  content: File; // File object (required by SDK)
299
420
  path: string; // Normalized path (webkitRelativePath or file.name)
300
421
  size: number; // File size in bytes
301
- md5?: string; // Pre-calculated MD5 checksum
422
+ md5?: string; // Undefined - Ship SDK calculates during deployment
302
423
 
303
424
  // ProcessedFile-specific properties (UI functionality)
304
425
  id: string; // Unique identifier for React keys
@@ -307,18 +428,8 @@ interface ProcessedFile extends StaticFile {
307
428
  type: string; // MIME type
308
429
  lastModified: number;
309
430
  status: FileStatus;
310
- statusMessage?: string;
311
- progress?: number; // Upload progress (0-100)
312
- }
313
-
314
- /**
315
- * ValidationConfig is an alias to ConfigResponse from @shipstatic/types
316
- * Use ship.getConfig() to get the exact validation limits from the server
317
- */
318
- interface ValidationConfig {
319
- maxFileSize: number; // Default: 5MB
320
- maxTotalSize: number; // Default: 25MB
321
- maxFilesCount: number; // Default: 100
431
+ statusMessage?: string; // Per-file error message
432
+ progress?: number; // Upload progress (0-100)
322
433
  }
323
434
 
324
435
  interface ClientError {
@@ -373,15 +484,39 @@ interface ProcessedFile extends StaticFile {
373
484
 
374
485
  ## Architecture Decisions
375
486
 
487
+ ### Why Drop Doesn't Calculate MD5
488
+
489
+ **Design Philosophy:** Drop should only provide what Ship SDK doesn't have.
490
+
491
+ **What Drop provides:**
492
+ - ✅ ZIP extraction (Ship SDK doesn't have this)
493
+ - ✅ React state management (Ship SDK doesn't have this)
494
+ - ✅ Folder structure preservation (UI-specific concern)
495
+ - ✅ Path normalization (UI-specific concern)
496
+
497
+ **What Ship SDK provides:**
498
+ - ✅ MD5 calculation (already implemented)
499
+ - ✅ Validation (already implemented)
500
+ - ✅ Deployment (core functionality)
501
+
502
+ **Why this matters:**
503
+ - Avoids duplicate MD5 calculation (performance)
504
+ - Single source of truth for deployment logic
505
+ - Drop stays focused on UI concerns
506
+ - Ship SDK handles all deployment concerns
507
+
508
+ **StaticFile.md5 is optional** - Ship SDK calculates it during deployment if not provided.
509
+
376
510
  ### Why Not Abstract?
377
511
 
378
512
  This package was extracted from the `web/drop` application and is purpose-built for Ship SDK. Key decisions:
379
513
 
380
- **1. Tightly Coupled to Ship SDK Requirements**
381
- - MD5 calculation is **mandatory** (Ship SDK requires it for integrity checks)
382
- - Common prefix stripping is **mandatory** (ensures clean deployment paths)
383
- - Folder structure preservation is **mandatory** (via `webkitRelativePath`)
384
- - These aren't optional features - they're essential for Ship deployments
514
+ **1. Focused on UI Concerns**
515
+ - ZIP extraction for user convenience
516
+ - File list state management for React UIs
517
+ - Folder structure preservation from drag-and-drop
518
+ - Path normalization for clean URLs
519
+ - These are UI/UX concerns, not deployment logic
385
520
 
386
521
  **2. Loosely Coupled Integration Pattern**
387
522
  Following industry standards (Firebase hooks, Supabase utilities), we chose:
@@ -413,14 +548,18 @@ We deliberately don't provide drop zone UI components because:
413
548
  - Your deployment UI is unique to your application
414
549
  - Providing a component that "works but loses paths" would be misleading
415
550
 
416
- ### Error Handling
551
+ ### Error Handling Philosophy
417
552
 
418
- MD5 calculation failures are properly handled:
419
- - Files with failed MD5 calculation are marked with `status: PROCESSING_ERROR`
420
- - The `statusMessage` contains the specific error details
421
- - These files are excluded from `getValidFiles()` and cannot be deployed
553
+ All errors are surfaced at the per-file level:
554
+ - Each file has its own `status` and `statusMessage` property
555
+ - Processing errors (e.g., ZIP extraction failures) are marked with `status: 'processing_error'`
556
+ - Validation failures are marked with `status: 'validation_failed'`
557
+ - The `statusMessage` always contains specific error details
558
+ - Failed files are excluded from `getValidFiles()` and cannot be deployed
422
559
  - No silent failures - all errors are visible to users
423
560
 
561
+ See the [Error Handling](#error-handling) section for examples of displaying per-file errors in your UI.
562
+
424
563
  ### Security
425
564
 
426
565
  **Path Sanitization**: ZIP extraction includes defense-in-depth protection against directory traversal attacks: