@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 +48 -564
- package/dist/index.cjs +7 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,76 +1,15 @@
|
|
|
1
1
|
# @shipstatic/drop
|
|
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, path normalization,
|
|
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
|
|
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 ? '
|
|
40
|
+
{drop.isDragging ? 'Drop here' : 'Click or drag files/folders'}
|
|
107
41
|
</div>
|
|
108
42
|
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
**Drop uses Ship's validation config automatically:**
|
|
53
|
+
## Features
|
|
138
54
|
|
|
139
|
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
stripPrefix?: boolean;
|
|
86
|
+
onValidationError?: (error: ClientError) => void;
|
|
87
|
+
stripPrefix?: boolean; // Strip common path prefix (default: true)
|
|
229
88
|
}
|
|
230
89
|
```
|
|
231
90
|
|
|
232
|
-
|
|
91
|
+
### Return Value
|
|
233
92
|
|
|
234
93
|
```typescript
|
|
235
94
|
interface DropReturn {
|
|
236
|
-
//
|
|
237
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
117
|
+
Drop uses Ship SDK's validation automatically:
|
|
317
118
|
|
|
318
119
|
```tsx
|
|
319
|
-
|
|
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
|
-
//
|
|
447
|
-
//
|
|
122
|
+
// Behind the scenes: ship.getConfig() → validateFiles()
|
|
123
|
+
// Client validation matches server limits
|
|
448
124
|
```
|
|
449
125
|
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
-
|
|
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
|
|