@shipstatic/drop 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -1,76 +1,15 @@
1
1
  # @shipstatic/drop
2
2
 
3
- **Headless file processing toolkit for Ship SDK deployments**
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, path normalization, and validation - everything needed before calling `ship.deploy()`.
6
-
7
- Built-in drag & drop support with prop getters! No need to manually implement drag & drop handlers - just spread `{...drop.getDropzoneProps()}` on your container and `{...drop.getInputProps()}` on the input element. Folder structure preservation is handled automatically.
8
-
9
- **Note:** MD5 calculation is handled by Ship SDK during deployment. Drop focuses on file processing and UI state management.
10
-
11
- ## Table of Contents
12
-
13
- - [Why Headless?](#why-headless)
14
- - [Features](#features)
15
- - [Installation](#installation)
16
- - [Requirements](#requirements)
17
- - [Quick Start](#quick-start)
18
- - [Configuration Architecture](#️-configuration-architecture)
19
- - [Advanced Usage](#advanced-programmatic-file-picker)
20
- - [API Reference](#api)
21
- - [State Machine](#state-machine)
22
- - [Error Handling](#error-handling)
23
- - [Types](#types)
24
- - [Ship SDK Integration](#direct-ship-sdk-integration)
25
- - [Architecture Decisions](#architecture-decisions)
26
-
27
- ## Why Headless?
28
-
29
- This package provides **zero UI components** - just a React hook with built-in drag & drop functionality. You bring your own styling.
30
-
31
- **What you get:**
32
- 1. **Built-in drag & drop** - Proper folder support with `webkitGetAsEntry` API, all handled internally
33
- 2. **Prop getters API** - Similar to `react-dropzone`, just spread props on your elements
34
- 3. **Full styling control** - No imposed CSS, design system, or theming
35
- 4. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
36
-
37
- **What's different from other libraries:**
38
- - Generic dropzone libraries don't preserve folder structure properly
39
- - We handle the complex parts (ZIP extraction, folder traversal, path normalization)
40
- - You handle the simple parts (styling, layout, animations)
41
-
42
- ## Features
43
-
44
- - 🎯 **Prop Getters API** - Just spread props on your elements (like `react-dropzone`)
45
- - 🖱️ **Built-in Drag & Drop** - Automatic folder support with `webkitGetAsEntry` API
46
- - 📦 **ZIP Support** - Automatic ZIP file extraction and processing
47
- - ✅ **Validation** - Client-side file size, count, and total size validation (powered by Ship SDK)
48
- - 🗑️ **Junk Filtering** - Automatically filters `.DS_Store`, `Thumbs.db`, etc. (powered by Ship SDK)
49
- - 🔒 **Path Sanitization** - Defense-in-depth protection against directory traversal attacks
50
- - 📁 **Folder Structure Preservation** - Proper folder paths via `webkitRelativePath`
51
- - 🎨 **Headless UI** - No visual components, just logic and state management
52
- - 📘 **Full TypeScript Support** - Complete type definitions with discriminated unions for state machine
53
- - 🚀 **Focused Scope** - File processing and UI state only. MD5 calculation and deployment handled by Ship SDK
5
+ A focused React hook for preparing files for deployment with [@shipstatic/ship](https://github.com/shipstatic/ship). Handles ZIP extraction, path normalization, folder structure preservation, and validation.
54
6
 
55
7
  ## Installation
56
8
 
57
9
  ```bash
58
- npm install @shipstatic/drop
59
- # or
60
- pnpm add @shipstatic/drop
10
+ npm install @shipstatic/drop @shipstatic/ship
61
11
  ```
62
12
 
63
- ## Requirements
64
-
65
- - **React**: ^18.0.0 or ^19.0.0
66
- - **TypeScript**: Full TypeScript support with exported types
67
- - **Browsers**: Modern browsers with support for:
68
- - File API (universal support)
69
- - DataTransfer API for drag & drop (universal support)
70
- - `webkitGetAsEntry` for folder uploads (Chrome, Edge, Safari 11.1+, Firefox 50+)
71
-
72
- **Note on folder uploads**: The folder drag & drop feature uses the `webkitGetAsEntry` API. While widely supported, older browsers may only support file-by-file selection. ZIP extraction works universally as a fallback.
73
-
74
13
  ## Quick Start
75
14
 
76
15
  ```tsx
@@ -79,19 +18,15 @@ import Ship from '@shipstatic/ship';
79
18
 
80
19
  const ship = new Ship({ deployToken: 'token-xxxx' });
81
20
 
82
- function MyUploader() {
83
- const drop = useDrop({
84
- ship // Pass Ship instance - Drop uses ship.getConfig() for validation
85
- });
21
+ function Uploader() {
22
+ const drop = useDrop({ ship });
86
23
 
87
24
  const handleUpload = async () => {
88
- // Extract File objects from ProcessedFiles for Ship SDK
89
25
  await ship.deployments.create(drop.validFiles.map(f => f.file));
90
26
  };
91
27
 
92
28
  return (
93
29
  <div>
94
- {/* Drag & drop zone with built-in folder support */}
95
30
  <div
96
31
  {...drop.getDropzoneProps()}
97
32
  style={{
@@ -99,32 +34,15 @@ function MyUploader() {
99
34
  borderColor: drop.isDragging ? 'blue' : 'gray',
100
35
  padding: '40px',
101
36
  textAlign: 'center',
102
- cursor: 'pointer',
103
37
  }}
104
38
  >
105
39
  <input {...drop.getInputProps()} />
106
- {drop.isDragging ? '📂 Drop here' : '📁 Click or drag files/folders'}
40
+ {drop.isDragging ? 'Drop here' : 'Click or drag files/folders'}
107
41
  </div>
108
42
 
109
- {/* Status - using state machine phase */}
110
- {drop.status && (
111
- <p>
112
- <strong>{drop.status.title}:</strong> {drop.status.details}
113
- </p>
114
- )}
115
-
116
- {/* File list */}
117
- {drop.files.map(file => (
118
- <div key={file.id}>
119
- {file.name} - {file.status}
120
- </div>
121
- ))}
122
-
123
- {/* Upload button */}
124
- <button
125
- onClick={handleUpload}
126
- disabled={drop.phase !== 'ready'}
127
- >
43
+ {drop.status && <p>{drop.status.title}: {drop.status.details}</p>}
44
+
45
+ <button onClick={handleUpload} disabled={drop.phase !== 'ready'}>
128
46
  Upload {drop.validFiles.length} files
129
47
  </button>
130
48
  </div>
@@ -132,525 +50,90 @@ function MyUploader() {
132
50
  }
133
51
  ```
134
52
 
135
- ### ⚠️ Configuration Architecture
136
-
137
- **Drop uses Ship's validation config automatically:**
53
+ ## Features
138
54
 
139
- Drop accepts a `Ship` instance and uses `ship.getConfig()` internally. This ensures:
140
- - **Single source of truth** - Validation config comes from Ship SDK
141
- - **Always in sync** - Client validation matches server limits
142
- - **No manual config fetching** - Drop handles it internally
143
- - **Simpler API** - Just pass `ship` instance
55
+ - **Prop Getters API** - Spread props on your elements (like `react-dropzone`)
56
+ - **Built-in Drag & Drop** - Folder support with `webkitGetAsEntry` API
57
+ - **ZIP Support** - Automatic extraction and processing
58
+ - **Ship SDK Integration** - Validation via `ship.getConfig()`
59
+ - **Headless** - No visual components, full styling control
60
+ - **TypeScript** - Complete type definitions
144
61
 
145
- ```tsx
146
- // Drop fetches config from Ship SDK automatically
147
- const drop = useDrop({ ship });
62
+ ## State Machine
148
63
 
149
- // Behind the scenes:
150
- // 1. Ship SDK fetches /config on initialization
151
- // 2. Drop calls ship.getConfig() when validating
152
- // 3. Validation always uses current server limits
153
64
  ```
154
-
155
- **Why this architecture:**
156
- - Drop has NO validation rules of its own - it's a pure proxy
157
- - Ship SDK is the single source of truth for validation
158
- - Drop only provides what Ship doesn't have (ZIP, React state, folder structure)
159
-
160
- ## Advanced: Programmatic File Picker
161
-
162
- You can programmatically trigger the file picker using the `open()` method:
163
-
164
- ```tsx
165
- function MyUploader() {
166
- const drop = useDrop({ ship });
167
-
168
- return (
169
- <div>
170
- {/* Custom trigger button */}
171
- <button onClick={drop.open}>
172
- Select Files
173
- </button>
174
-
175
- {/* Hidden input managed by the hook */}
176
- <input {...drop.getInputProps()} />
177
-
178
- {/* Or use the dropzone */}
179
- <div {...drop.getDropzoneProps()}>
180
- Drop files here
181
- </div>
182
- </div>
183
- );
184
- }
65
+ idle → dragging → processing → ready/error
185
66
  ```
186
67
 
187
- ## Advanced: Manual File Processing
188
-
189
- For advanced use cases, you can manually process files instead of using prop getters:
190
-
191
68
  ```tsx
192
- function AdvancedUploader() {
193
- const drop = useDrop({ ship });
194
-
195
- const handleManualDrop = async (e: React.DragEvent) => {
196
- e.preventDefault();
197
- const files = Array.from(e.dataTransfer.files);
198
- await drop.processFiles(files);
199
- };
200
-
201
- return (
202
- <div onDrop={handleManualDrop}>
203
- {/* Custom implementation */}
204
- </div>
205
- );
69
+ switch (drop.phase) {
70
+ case 'idle': return 'Drop files here';
71
+ case 'dragging': return 'Drop now!';
72
+ case 'processing': return 'Processing...';
73
+ case 'ready': return `${drop.validFiles.length} files ready`;
74
+ case 'error': return drop.status?.details;
206
75
  }
207
76
  ```
208
77
 
209
- **Note:** When using manual processing, you lose automatic folder structure preservation. The built-in `getDropzoneProps()` handles `webkitGetAsEntry` API internally to preserve folder paths.
210
-
211
78
  ## API
212
79
 
213
- ### `useDrop(options?)`
214
-
215
- Main hook for managing drop state.
216
-
217
- **Options:**
80
+ ### `useDrop(options)`
218
81
 
219
82
  ```typescript
220
83
  interface DropOptions {
221
- /** Ship SDK instance (required for validation) */
222
- ship: Ship;
223
- /** Callback when validation fails */
224
- onValidationError?: (error: ClientError) => void;
225
- /** Callback when files are ready for upload */
84
+ ship: Ship; // Ship SDK instance (required)
226
85
  onFilesReady?: (files: ProcessedFile[]) => void;
227
- /** Whether to strip common directory prefix from paths (default: true) */
228
- stripPrefix?: boolean;
86
+ onValidationError?: (error: ClientError) => void;
87
+ stripPrefix?: boolean; // Strip common path prefix (default: true)
229
88
  }
230
89
  ```
231
90
 
232
- **Returns:**
91
+ ### Return Value
233
92
 
234
- ```typescript
235
93
  ```typescript
236
94
  interface DropReturn {
237
- // Convenience getters (computed from state)
238
- /** Current phase of the state machine */
239
- phase: DropStateValue;
240
- /** Whether currently processing files (ZIP extraction, etc.) */
95
+ // State
96
+ phase: 'idle' | 'dragging' | 'processing' | 'ready' | 'error';
241
97
  isProcessing: boolean;
242
- /** Whether user is currently dragging over the dropzone */
243
98
  isDragging: boolean;
244
- /** Flattened access to files */
245
99
  files: ProcessedFile[];
246
- /** Flattened access to source name */
247
- sourceName: string;
248
- /** Flattened access to status */
249
- status: DropStatus | null;
250
-
251
- // Primary API: Prop getters for easy integration
252
- /** Get props to spread on dropzone element (handles drag & drop) */
253
- getDropzoneProps: () => {
254
- onDragOver: (e: React.DragEvent) => void;
255
- onDragLeave: (e: React.DragEvent) => void;
256
- onDrop: (e: React.DragEvent) => void;
257
- onClick: () => void;
258
- };
259
- /** Get props to spread on hidden file input element */
260
- getInputProps: () => {
261
- ref: React.RefObject<HTMLInputElement | null>;
262
- type: 'file';
263
- style: { display: string };
264
- multiple: boolean;
265
- webkitdirectory: string; // Note: React expects string ('') for boolean attributes
266
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
267
- };
100
+ validFiles: ProcessedFile[];
101
+ status: { title: string; details: string } | null;
102
+
103
+ // Prop getters
104
+ getDropzoneProps: () => { onDragOver, onDragLeave, onDrop, onClick };
105
+ getInputProps: () => { ref, type, style, multiple, webkitdirectory, onChange };
268
106
 
269
107
  // Actions
270
- /** Programmatically trigger file picker */
271
- open: () => void;
272
- /** Manually process files (for advanced usage) */
108
+ open: () => void; // Trigger file picker
273
109
  processFiles: (files: File[]) => Promise<void>;
274
- /** Clear all files and reset state */
275
110
  clearAll: () => void;
276
-
277
- // Helpers
278
- /** Get only valid files ready for upload */
279
- validFiles: ProcessedFile[];
280
- /** Update upload state for a specific file (status, progress, message) */
281
- updateFileStatus: (fileId: string, state: {
282
- status: FileStatus;
283
- statusMessage?: string;
284
- progress?: number;
285
- }) => void;
286
- }
287
-
288
- // State machine types
289
- type DropStateValue =
290
- | 'idle' // The hook is ready for files
291
- | 'dragging' // The user is dragging files over the dropzone
292
- | 'processing' // Files are being validated and processed
293
- | 'ready' // Files are valid and ready for deployment
294
- | 'error'; // An error occurred during processing
295
-
296
- interface DropStatus {
297
- title: string;
298
- details: string;
299
- errors?: string[];
300
- }
301
- ```
302
-
303
- ## State Machine
304
-
305
- The drop hook uses a state machine for predictable, clear state management. Instead of multiple boolean flags, you have a single `drop.phase` that represents exactly what's happening.
306
-
307
- ### State Flow
308
-
309
- ```
310
- idle → dragging → idle (drag leave without drop)
311
- idle → dragging → processing → ready (successful)
312
- idle → dragging → processing → error (failed)
313
- ready → dragging → processing → ... (new drop)
314
- error → dragging → processing → ... (retry)
315
- ```
316
-
317
- ### Using the State Machine
318
-
319
- ```tsx
320
- function StatusIndicator({ drop }) {
321
- // Use drop.phase to switch-case on the state
322
- switch (drop.phase) {
323
- case 'idle':
324
- return <p>Drop files here or click to select</p>;
325
-
326
- case 'dragging':
327
- return <p>Drop your files now!</p>;
328
-
329
- case 'processing':
330
- return <p>{drop.status?.details || 'Processing...'}</p>;
331
-
332
- case 'ready':
333
- return (
334
- <div>
335
- <p>✓ {drop.files.length} files ready</p>
336
- <button>Upload to Ship</button>
337
- </div>
338
- );
339
-
340
- case 'error':
341
- return (
342
- <div>
343
- <p>✗ {drop.status?.title}</p>
344
- <p>{drop.status?.details}</p>
345
- <button onClick={drop.clearAll}>Try Again</button>
346
- </div>
347
- );
348
- }
349
- }
350
- ```
351
-
352
- ### Convenience Getters
353
-
354
- For simpler use cases, boolean convenience getters are provided:
355
-
356
- ```tsx
357
- // These are computed from drop.phase (read-only projections)
358
- drop.isProcessing // true when phase === 'processing'
359
- drop.isDragging // true when phase === 'dragging'
360
-
361
- // For error information, use the flattened status object
362
- drop.phase === 'error' // Check if in error state
363
- drop.status?.title // Error title
364
- drop.status?.details // Error details
365
- ```
366
-
367
- ### Benefits
368
-
369
- - **No impossible states** - Can't be `isProcessing=true` AND `isDragging=true`
370
- - **Clear transitions** - State flow is explicit and predictable
371
- - **Better TypeScript** - Discriminated unions provide type safety
372
- - **Easier debugging** - Single source of truth for what's happening
373
-
374
- ## Error Handling
375
-
376
- ### Per-File Error Display
377
-
378
- Each file in the `state.files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
379
-
380
- ```tsx
381
- function FileList({ drop }) {
382
- return (
383
- <div>
384
- {/* Flattened access to files array */}
385
- {drop.files.map(file => (
386
- <div key={file.id}>
387
- <span>{file.path}</span>
388
-
389
- {/* Show status indicator */}
390
- {file.status === 'ready' ? '✓' : '✗'}
391
-
392
- {/* Show per-file error message */}
393
- {file.status !== 'ready' && file.statusMessage && (
394
- <span style={{ color: 'red' }}>
395
- {file.statusMessage}
396
- </span>
397
- )}
398
- </div>
399
- ))}
400
-
401
- {/* If validation fails, allow user to clear all and try again */}
402
- {drop.phase === 'error' && (
403
- <button onClick={drop.clearAll}>
404
- Clear All & Try Again
405
- </button>
406
- )}
407
- </div>
408
- );
111
+ updateFileStatus: (fileId: string, state: {...}) => void;
409
112
  }
410
113
  ```
411
114
 
412
- **Common error statuses:**
413
- - `validation_failed` - File failed validation (size, type, name, etc.)
414
- - `processing_error` - MD5 calculation or processing failed
415
- - `empty_file` - File is 0 bytes
416
- - `ready` - File passed all validation and is ready for upload
417
-
418
- ### Error State Summary
419
-
420
- When files fail validation or processing, check the error state:
421
-
422
- ```tsx
423
- {drop.phase === 'error' && drop.status && (
424
- <div>
425
- <p>{drop.status.title}</p>
426
- <p>{drop.status.details}</p>
427
- </div>
428
- )}
429
- ```
430
-
431
- **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.
432
-
433
- ### No Individual File Removal
434
-
435
- The Drop package **intentionally does not support removing individual files**. Here's why:
115
+ ## Ship SDK Integration
436
116
 
437
- **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.
117
+ Drop uses Ship SDK's validation automatically:
438
118
 
439
- **The Problem with Individual Removal:**
440
119
  ```tsx
441
- // User drops 5 files, 1 is too large
442
- // Atomic validation: ALL 5 files marked as validation_failed
443
-
444
- // If we allowed removing the large file:
445
- drop.removeFile(largeFileId); // ❌ We don't support this!
120
+ const drop = useDrop({ ship });
446
121
 
447
- // Would need to re-validate remaining 4 files
448
- // Creates complexity and race conditions
122
+ // Behind the scenes: ship.getConfig() validateFiles()
123
+ // Client validation matches server limits
449
124
  ```
450
125
 
451
- **The Simple Solution:**
452
- Use `clearAll()` to reset and try again:
126
+ Pass files to Ship SDK:
453
127
 
454
128
  ```tsx
455
- // If validation fails, show user which files failed
456
- {drop.phase === 'error' && (
457
- <div>
458
- <p>Validation failed. Please fix the issues and try again:</p>
459
- {drop.files.map(file => (
460
- <div key={file.id}>
461
- {file.path}: {file.statusMessage}
462
- </div>
463
- ))}
464
- <button onClick={drop.clearAll}>Clear All & Try Again</button>
465
- </div>
466
- )}
467
- ```
468
-
469
- **Benefits:**
470
- - ✅ No race conditions or stale validation state
471
- - ✅ Simpler mental model (atomic = all-or-nothing)
472
- - ✅ Aligns with Ship SDK's validation philosophy
473
- - ✅ Clear UX: fix the problem, then re-drop
474
-
475
- ## Types
476
-
477
- ```typescript
478
- /**
479
- * ProcessedFile - a file processed by Drop, ready for Ship SDK deployment
480
- *
481
- * Use the `file` property to pass to ship.deployments.create()
482
- * Note: md5 is intentionally undefined - Ship SDK calculates it during deployment
483
- */
484
- interface ProcessedFile {
485
- /** Unique identifier for React keys */
486
- id: string;
487
- /** The File object - pass this to ship.deployments.create() */
488
- file: File;
489
- /** Relative path for deployment (e.g., "images/photo.jpg") */
490
- path: string;
491
- /** File size in bytes */
492
- size: number;
493
- /** MD5 hash (optional - Ship SDK calculates during deployment if not provided) */
494
- md5?: string;
495
- /** Filename without path */
496
- name: string;
497
- /** MIME type for UI icons/previews */
498
- type: string;
499
- /** Last modified timestamp */
500
- lastModified: number;
501
- /** Current processing/upload status */
502
- status: FileStatus;
503
- /** Human-readable status message for UI */
504
- statusMessage?: string;
505
- /** Upload progress (0-100) - only set during upload */
506
- progress?: number;
507
- }
508
-
509
- interface ClientError {
510
- error: string;
511
- details: string;
512
- errors: string[];
513
- isClientError: true;
514
- }
515
-
516
- type FileStatus =
517
- | 'pending'
518
- | 'ready'
519
- | 'uploading'
520
- | 'complete'
521
- | 'processing_error'
522
- | 'error'
523
- | 'validation_failed'
524
- | 'empty_file';
525
- ```
526
-
527
- ## Direct Ship SDK Integration
528
-
529
- Extract the `file` property from ProcessedFiles to pass to Ship SDK:
530
-
531
- ```typescript
532
- // Get valid files and extract File objects for deployment
533
129
  const filesToDeploy = drop.validFiles.map(f => f.file);
534
-
535
- // Pass File[] to Ship SDK
536
130
  await ship.deployments.create(filesToDeploy);
537
131
  ```
538
132
 
539
- ### Why Extract `.file`?
540
-
541
- Ship SDK's browser `deployments.create()` accepts `File[]`. Drop's `ProcessedFile` wraps the File with additional UI metadata (id, status, progress). The `.file` property gives you the raw File object that Ship SDK expects.
542
-
543
- ```typescript
544
- // ProcessedFile structure
545
- interface ProcessedFile {
546
- file: File; // ← Pass this to Ship SDK
547
- path: string; // Normalized path (set on file.webkitRelativePath)
548
- // ... UI properties (id, status, progress, etc.)
549
- }
550
- ```
551
-
552
- **Important**: Drop automatically sets `webkitRelativePath` on each File to preserve folder structure. Ship SDK reads this property during deployment, so paths are handled correctly.
553
-
554
- ## Architecture Decisions
555
-
556
- ### Why Drop Doesn't Calculate MD5
557
-
558
- **Design Philosophy:** Drop should only provide what Ship SDK doesn't have.
559
-
560
- **What Drop provides:**
561
- - ✅ ZIP extraction (Ship SDK doesn't have this)
562
- - ✅ React state management (Ship SDK doesn't have this)
563
- - ✅ Folder structure preservation (UI-specific concern)
564
- - ✅ Path normalization (UI-specific concern)
565
-
566
- **What Ship SDK provides:**
567
- - ✅ MD5 calculation (already implemented)
568
- - ✅ Validation (already implemented)
569
- - ✅ Deployment (core functionality)
570
-
571
- **Why this matters:**
572
- - Avoids duplicate MD5 calculation (performance)
573
- - Single source of truth for deployment logic
574
- - Drop stays focused on UI concerns
575
- - Ship SDK handles all deployment concerns
576
-
577
- **StaticFile.md5 is optional** - Ship SDK calculates it during deployment if not provided.
578
-
579
- ### Why Not Abstract?
580
-
581
- This package was extracted from the `web/drop` application and is purpose-built for Ship SDK. Key decisions:
582
-
583
- **1. Focused on UI Concerns**
584
- - ZIP extraction for user convenience
585
- - File list state management for React UIs
586
- - Drag & drop event handling with folder support
587
- - Folder structure preservation via `webkitGetAsEntry` API
588
- - Path normalization for clean URLs
589
- - These are UI/UX concerns, not deployment logic
590
-
591
- **2. Prop Getters Pattern (Like react-dropzone)**
592
- We provide event handlers, not visual components:
593
- - ✅ **`getDropzoneProps()`** - Returns drag & drop event handlers
594
- - ✅ **`getInputProps()`** - Returns file input configuration
595
- - ✅ **`isDragging`** - State for visual feedback
596
- - ✅ **You control the DOM** - Your markup, your styling, your design system
597
-
598
- This is the same pattern used by popular libraries like `react-dropzone`, `downshift`, and `react-table`.
599
-
600
- **3. Loosely Coupled Integration**
601
- Following industry standards (Firebase hooks, Supabase utilities), we chose:
602
- - ✅ **Ship instance as dependency**: Validates using `ship.getConfig()`
603
- - ✅ **Simple output**: Extract `.file` from ProcessedFile[] for Ship SDK
604
- - ✅ **Testable**: Easy to mock Ship SDK for testing
605
- - ✅ **Flexible**: Host app controls WHEN to deploy
606
-
607
- **4. Type System Integration**
608
-
609
- ProcessedFile wraps File objects with UI-specific metadata:
610
-
611
- ```
612
- File[] → ProcessedFile[] → .map(f => f.file) → ship.deployments.create()
613
- ```
614
-
615
- ProcessedFile adds UI properties (id, name, status, progress) while preserving the original File object. The `.file` property gives you direct access for Ship SDK deployment.
616
-
617
- **5. No Visual Components**
618
-
619
- We deliberately don't provide styled components because:
620
- - Your design system is unique to your application
621
- - Styling should match your brand, not our opinions
622
- - Prop getters give you full control over DOM structure and CSS
623
- - Similar to how `react-dropzone` works - logic without opinions
624
-
625
- ### Error Handling Philosophy
626
-
627
- All errors are surfaced at the per-file level:
628
- - Each file has its own `status` and `statusMessage` property
629
- - Processing errors (e.g., ZIP extraction failures) are marked with `status: 'processing_error'`
630
- - Validation failures are marked with `status: 'validation_failed'`
631
- - The `statusMessage` always contains specific error details
632
- - Failed files are excluded from `validFiles` and cannot be deployed
633
- - No silent failures - all errors are visible to users
634
-
635
- See the [Error Handling](#error-handling) section for examples of displaying per-file errors in your UI.
636
-
637
- ### Security
638
-
639
- **Path Sanitization**: ZIP extraction includes defense-in-depth protection against directory traversal attacks:
640
- - Normalizes all file paths by removing `..`, `.`, and empty segments
641
- - Prevents traversal above the root directory
642
- - Converts absolute paths to relative paths
643
- - Skips files that resolve to empty paths after normalization
644
- - Comprehensive test coverage for various attack vectors
645
-
646
- While the Ship SDK validates paths server-side, client-side sanitization provides an additional security layer and prevents malicious paths from ever reaching the server.
133
+ ## Requirements
647
134
 
648
- **Concurrency Protection**: The `processFiles` function includes built-in race condition protection:
649
- - Uses a synchronous ref guard to prevent concurrent processing
650
- - Automatically ignores duplicate calls while processing is in progress
651
- - Logs warnings when concurrent calls are detected
652
- - Ensures the processing flag is always cleared, even on errors
653
- - Makes the hook robust regardless of UI implementation
135
+ - React 18+ or 19+
136
+ - Modern browsers (Chrome, Edge, Safari 11.1+, Firefox 50+)
654
137
 
655
138
  ## License
656
139