@shipstatic/drop 0.1.14 → 0.1.16

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,524 +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
93
  ```typescript
235
94
  interface DropReturn {
236
- // Convenience getters (computed from state)
237
- /** Current phase of the state machine */
238
- phase: DropStateValue;
239
- /** Whether currently processing files (ZIP extraction, etc.) */
95
+ // State
96
+ phase: 'idle' | 'dragging' | 'processing' | 'ready' | 'error';
240
97
  isProcessing: boolean;
241
- /** Whether user is currently dragging over the dropzone */
242
98
  isDragging: boolean;
243
- /** Flattened access to files */
244
99
  files: ProcessedFile[];
245
- /** Flattened access to source name */
246
- sourceName: string;
247
- /** Flattened access to status */
248
- status: DropStatus | null;
249
-
250
- // Primary API: Prop getters for easy integration
251
- /** Get props to spread on dropzone element (handles drag & drop) */
252
- getDropzoneProps: () => {
253
- onDragOver: (e: React.DragEvent) => void;
254
- onDragLeave: (e: React.DragEvent) => void;
255
- onDrop: (e: React.DragEvent) => void;
256
- onClick: () => void;
257
- };
258
- /** Get props to spread on hidden file input element */
259
- getInputProps: () => {
260
- ref: React.RefObject<HTMLInputElement | null>;
261
- type: 'file';
262
- style: { display: string };
263
- multiple: boolean;
264
- webkitdirectory: string; // Note: React expects string ('') for boolean attributes
265
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
266
- };
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 };
267
106
 
268
107
  // Actions
269
- /** Programmatically trigger file picker */
270
- open: () => void;
271
- /** Manually process files (for advanced usage) */
108
+ open: () => void; // Trigger file picker
272
109
  processFiles: (files: File[]) => Promise<void>;
273
- /** Clear all files and reset state */
274
110
  clearAll: () => void;
275
-
276
- // Helpers
277
- /** Get only valid files ready for upload */
278
- validFiles: ProcessedFile[];
279
- /** Update upload state for a specific file (status, progress, message) */
280
- updateFileStatus: (fileId: string, state: {
281
- status: FileStatus;
282
- statusMessage?: string;
283
- progress?: number;
284
- }) => void;
285
- }
286
-
287
- // State machine types
288
- type DropStateValue =
289
- | 'idle' // The hook is ready for files
290
- | 'dragging' // The user is dragging files over the dropzone
291
- | 'processing' // Files are being validated and processed
292
- | 'ready' // Files are valid and ready for deployment
293
- | 'error'; // An error occurred during processing
294
-
295
- interface DropStatus {
296
- title: string;
297
- details: string;
298
- errors?: string[];
111
+ updateFileStatus: (fileId: string, state: {...}) => void;
299
112
  }
300
113
  ```
301
114
 
302
- ## State Machine
303
-
304
- 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.
305
-
306
- ### State Flow
307
-
308
- ```
309
- idle → dragging → idle (drag leave without drop)
310
- idle → dragging → processing → ready (successful)
311
- idle → dragging → processing → error (failed)
312
- ready → dragging → processing → ... (new drop)
313
- error → dragging → processing → ... (retry)
314
- ```
115
+ ## Ship SDK Integration
315
116
 
316
- ### Using the State Machine
117
+ Drop uses Ship SDK's validation automatically:
317
118
 
318
119
  ```tsx
319
- function StatusIndicator({ drop }) {
320
- // Use drop.phase to switch-case on the state
321
- switch (drop.phase) {
322
- case 'idle':
323
- return <p>Drop files here or click to select</p>;
324
-
325
- case 'dragging':
326
- return <p>Drop your files now!</p>;
327
-
328
- case 'processing':
329
- return <p>{drop.status?.details || 'Processing...'}</p>;
330
-
331
- case 'ready':
332
- return (
333
- <div>
334
- <p>✓ {drop.files.length} files ready</p>
335
- <button>Upload to Ship</button>
336
- </div>
337
- );
338
-
339
- case 'error':
340
- return (
341
- <div>
342
- <p>✗ {drop.status?.title}</p>
343
- <p>{drop.status?.details}</p>
344
- <button onClick={drop.clearAll}>Try Again</button>
345
- </div>
346
- );
347
- }
348
- }
349
- ```
350
-
351
- ### Convenience Getters
352
-
353
- For simpler use cases, boolean convenience getters are provided:
354
-
355
- ```tsx
356
- // These are computed from drop.phase (read-only projections)
357
- drop.isProcessing // true when phase === 'processing'
358
- drop.isDragging // true when phase === 'dragging'
359
-
360
- // For error information, use the flattened status object
361
- drop.phase === 'error' // Check if in error state
362
- drop.status?.title // Error title
363
- drop.status?.details // Error details
364
- ```
365
-
366
- ### Benefits
367
-
368
- - **No impossible states** - Can't be `isProcessing=true` AND `isDragging=true`
369
- - **Clear transitions** - State flow is explicit and predictable
370
- - **Better TypeScript** - Discriminated unions provide type safety
371
- - **Easier debugging** - Single source of truth for what's happening
372
-
373
- ## Error Handling
374
-
375
- ### Per-File Error Display
376
-
377
- Each file in the `state.files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
378
-
379
- ```tsx
380
- function FileList({ drop }) {
381
- return (
382
- <div>
383
- {/* Flattened access to files array */}
384
- {drop.files.map(file => (
385
- <div key={file.id}>
386
- <span>{file.path}</span>
387
-
388
- {/* Show status indicator */}
389
- {file.status === 'ready' ? '✓' : '✗'}
390
-
391
- {/* Show per-file error message */}
392
- {file.status !== 'ready' && file.statusMessage && (
393
- <span style={{ color: 'red' }}>
394
- {file.statusMessage}
395
- </span>
396
- )}
397
- </div>
398
- ))}
399
-
400
- {/* If validation fails, allow user to clear all and try again */}
401
- {drop.phase === 'error' && (
402
- <button onClick={drop.clearAll}>
403
- Clear All & Try Again
404
- </button>
405
- )}
406
- </div>
407
- );
408
- }
409
- ```
410
-
411
- **Common error statuses:**
412
- - `validation_failed` - File failed validation (size, type, name, etc.)
413
- - `processing_error` - MD5 calculation or processing failed
414
- - `empty_file` - File is 0 bytes
415
- - `ready` - File passed all validation and is ready for upload
416
-
417
- ### Error State Summary
418
-
419
- When files fail validation or processing, check the error state:
420
-
421
- ```tsx
422
- {drop.phase === 'error' && drop.status && (
423
- <div>
424
- <p>{drop.status.title}</p>
425
- <p>{drop.status.details}</p>
426
- </div>
427
- )}
428
- ```
429
-
430
- **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.
431
-
432
- ### No Individual File Removal
433
-
434
- The Drop package **intentionally does not support removing individual files**. Here's why:
435
-
436
- **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.
437
-
438
- **The Problem with Individual Removal:**
439
- ```tsx
440
- // User drops 5 files, 1 is too large
441
- // Atomic validation: ALL 5 files marked as validation_failed
442
-
443
- // If we allowed removing the large file:
444
- drop.removeFile(largeFileId); // ❌ We don't support this!
120
+ const drop = useDrop({ ship });
445
121
 
446
- // Would need to re-validate remaining 4 files
447
- // Creates complexity and race conditions
122
+ // Behind the scenes: ship.getConfig() validateFiles()
123
+ // Client validation matches server limits
448
124
  ```
449
125
 
450
- **The Simple Solution:**
451
- Use `clearAll()` to reset and try again:
126
+ Pass files to Ship SDK:
452
127
 
453
128
  ```tsx
454
- // If validation fails, show user which files failed
455
- {drop.phase === 'error' && (
456
- <div>
457
- <p>Validation failed. Please fix the issues and try again:</p>
458
- {drop.files.map(file => (
459
- <div key={file.id}>
460
- {file.path}: {file.statusMessage}
461
- </div>
462
- ))}
463
- <button onClick={drop.clearAll}>Clear All & Try Again</button>
464
- </div>
465
- )}
466
- ```
467
-
468
- **Benefits:**
469
- - ✅ No race conditions or stale validation state
470
- - ✅ Simpler mental model (atomic = all-or-nothing)
471
- - ✅ Aligns with Ship SDK's validation philosophy
472
- - ✅ Clear UX: fix the problem, then re-drop
473
-
474
- ## Types
475
-
476
- ```typescript
477
- /**
478
- * ProcessedFile - a file processed by Drop, ready for Ship SDK deployment
479
- *
480
- * Use the `file` property to pass to ship.deployments.create()
481
- * Note: md5 is intentionally undefined - Ship SDK calculates it during deployment
482
- */
483
- interface ProcessedFile {
484
- /** Unique identifier for React keys */
485
- id: string;
486
- /** The File object - pass this to ship.deployments.create() */
487
- file: File;
488
- /** Relative path for deployment (e.g., "images/photo.jpg") */
489
- path: string;
490
- /** File size in bytes */
491
- size: number;
492
- /** MD5 hash (optional - Ship SDK calculates during deployment if not provided) */
493
- md5?: string;
494
- /** Filename without path */
495
- name: string;
496
- /** MIME type for UI icons/previews */
497
- type: string;
498
- /** Last modified timestamp */
499
- lastModified: number;
500
- /** Current processing/upload status */
501
- status: FileStatus;
502
- /** Human-readable status message for UI */
503
- statusMessage?: string;
504
- /** Upload progress (0-100) - only set during upload */
505
- progress?: number;
506
- }
507
-
508
- interface ClientError {
509
- error: string;
510
- details: string;
511
- errors: string[];
512
- isClientError: true;
513
- }
514
-
515
- type FileStatus =
516
- | 'pending'
517
- | 'ready'
518
- | 'uploading'
519
- | 'complete'
520
- | 'processing_error'
521
- | 'error'
522
- | 'validation_failed'
523
- | 'empty_file';
524
- ```
525
-
526
- ## Direct Ship SDK Integration
527
-
528
- Extract the `file` property from ProcessedFiles to pass to Ship SDK:
529
-
530
- ```typescript
531
- // Get valid files and extract File objects for deployment
532
129
  const filesToDeploy = drop.validFiles.map(f => f.file);
533
-
534
- // Pass File[] to Ship SDK
535
130
  await ship.deployments.create(filesToDeploy);
536
131
  ```
537
132
 
538
- ### Why Extract `.file`?
539
-
540
- 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.
541
-
542
- ```typescript
543
- // ProcessedFile structure
544
- interface ProcessedFile {
545
- file: File; // ← Pass this to Ship SDK
546
- path: string; // Normalized path (set on file.webkitRelativePath)
547
- // ... UI properties (id, status, progress, etc.)
548
- }
549
- ```
550
-
551
- **Important**: Drop automatically sets `webkitRelativePath` on each File to preserve folder structure. Ship SDK reads this property during deployment, so paths are handled correctly.
552
-
553
- ## Architecture Decisions
554
-
555
- ### Why Drop Doesn't Calculate MD5
556
-
557
- **Design Philosophy:** Drop should only provide what Ship SDK doesn't have.
558
-
559
- **What Drop provides:**
560
- - ✅ ZIP extraction (Ship SDK doesn't have this)
561
- - ✅ React state management (Ship SDK doesn't have this)
562
- - ✅ Folder structure preservation (UI-specific concern)
563
- - ✅ Path normalization (UI-specific concern)
564
-
565
- **What Ship SDK provides:**
566
- - ✅ MD5 calculation (already implemented)
567
- - ✅ Validation (already implemented)
568
- - ✅ Deployment (core functionality)
569
-
570
- **Why this matters:**
571
- - Avoids duplicate MD5 calculation (performance)
572
- - Single source of truth for deployment logic
573
- - Drop stays focused on UI concerns
574
- - Ship SDK handles all deployment concerns
575
-
576
- **StaticFile.md5 is optional** - Ship SDK calculates it during deployment if not provided.
577
-
578
- ### Why Not Abstract?
579
-
580
- This package was extracted from the `web/drop` application and is purpose-built for Ship SDK. Key decisions:
581
-
582
- **1. Focused on UI Concerns**
583
- - ZIP extraction for user convenience
584
- - File list state management for React UIs
585
- - Drag & drop event handling with folder support
586
- - Folder structure preservation via `webkitGetAsEntry` API
587
- - Path normalization for clean URLs
588
- - These are UI/UX concerns, not deployment logic
589
-
590
- **2. Prop Getters Pattern (Like react-dropzone)**
591
- We provide event handlers, not visual components:
592
- - ✅ **`getDropzoneProps()`** - Returns drag & drop event handlers
593
- - ✅ **`getInputProps()`** - Returns file input configuration
594
- - ✅ **`isDragging`** - State for visual feedback
595
- - ✅ **You control the DOM** - Your markup, your styling, your design system
596
-
597
- This is the same pattern used by popular libraries like `react-dropzone`, `downshift`, and `react-table`.
598
-
599
- **3. Loosely Coupled Integration**
600
- Following industry standards (Firebase hooks, Supabase utilities), we chose:
601
- - ✅ **Ship instance as dependency**: Validates using `ship.getConfig()`
602
- - ✅ **Simple output**: Extract `.file` from ProcessedFile[] for Ship SDK
603
- - ✅ **Testable**: Easy to mock Ship SDK for testing
604
- - ✅ **Flexible**: Host app controls WHEN to deploy
605
-
606
- **4. Type System Integration**
607
-
608
- ProcessedFile wraps File objects with UI-specific metadata:
609
-
610
- ```
611
- File[] → ProcessedFile[] → .map(f => f.file) → ship.deployments.create()
612
- ```
613
-
614
- 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.
615
-
616
- **5. No Visual Components**
617
-
618
- We deliberately don't provide styled components because:
619
- - Your design system is unique to your application
620
- - Styling should match your brand, not our opinions
621
- - Prop getters give you full control over DOM structure and CSS
622
- - Similar to how `react-dropzone` works - logic without opinions
623
-
624
- ### Error Handling Philosophy
625
-
626
- All errors are surfaced at the per-file level:
627
- - Each file has its own `status` and `statusMessage` property
628
- - Processing errors (e.g., ZIP extraction failures) are marked with `status: 'processing_error'`
629
- - Validation failures are marked with `status: 'validation_failed'`
630
- - The `statusMessage` always contains specific error details
631
- - Failed files are excluded from `validFiles` and cannot be deployed
632
- - No silent failures - all errors are visible to users
633
-
634
- See the [Error Handling](#error-handling) section for examples of displaying per-file errors in your UI.
635
-
636
- ### Security
637
-
638
- **Path Sanitization**: ZIP extraction includes defense-in-depth protection against directory traversal attacks:
639
- - Normalizes all file paths by removing `..`, `.`, and empty segments
640
- - Prevents traversal above the root directory
641
- - Converts absolute paths to relative paths
642
- - Skips files that resolve to empty paths after normalization
643
- - Comprehensive test coverage for various attack vectors
644
-
645
- 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
646
134
 
647
- **Concurrency Protection**: The `processFiles` function includes built-in race condition protection:
648
- - Uses a synchronous ref guard to prevent concurrent processing
649
- - Automatically ignores duplicate calls while processing is in progress
650
- - Logs warnings when concurrent calls are detected
651
- - Ensures the processing flag is always cleared, even on errors
652
- - Makes the hook robust regardless of UI implementation
135
+ - React 18+ or 19+
136
+ - Modern browsers (Chrome, Edge, Safari 11.1+, Firefox 50+)
653
137
 
654
138
  ## License
655
139