Headless file processing toolkit for Ship SDK deployments.
A focused React hook for preparing files for deployment with @shipstatic/ship. Handles ZIP extraction, path normalization, folder structure preservation, and validation.
npm install @shipstatic/drop @shipstatic/shipimport { useDrop } from '@shipstatic/drop';
import Ship from '@shipstatic/ship';
const ship = new Ship({ deployToken: 'token-xxxx' });
function Uploader() {
const drop = useDrop({ ship });
const handleUpload = async () => {
const files = drop.getFilesForUpload();
await ship.deployments.upload(files);
};
return (
<div>
<div
{...drop.getDropzoneProps()}
style={{
border: '2px dashed',
borderColor: drop.isDragging ? 'blue' : 'gray',
padding: '40px',
textAlign: 'center',
}}
>
<input {...drop.getInputProps()} />
{drop.isDragging ? 'Drop here' : 'Click or drag files/folders'}
</div>
{drop.status && <p>{drop.status.title}: {drop.status.details}</p>}
<button onClick={handleUpload} disabled={!drop.validFiles.length}>
Upload {drop.validFiles.length} files
</button>
</div>
);
}- Prop Getters API - Spread props on your elements (like
react-dropzone) - Built-in Drag & Drop - Folder support with
webkitGetAsEntryAPI - ZIP Support - Automatic extraction and processing
- Ship SDK Integration - Validation via
ship.getLimits() - Headless - No visual components, full styling control
- TypeScript - Complete type definitions
idle → dragging → processing → ready/error
Use semantic booleans for clean rendering:
{drop.isProcessing && <Spinner />}
{drop.hasError && <Error message={drop.status?.details} onRetry={drop.reset} />}
{drop.isInteractive && <DropZone />}Or use phase for switch-case logic:
switch (drop.phase) {
case 'idle': return 'Drop files here';
case 'dragging': return 'Drop now!';
case 'processing': return 'Processing...';
case 'ready': return `${drop.validFiles.length} files ready`;
case 'error': return drop.status?.details;
}interface DropOptions {
ship: Ship; // Ship SDK instance (required)
onFilesReady?: (files: ProcessedFile[]) => void;
onValidationError?: (error: ClientError) => void;
stripPrefix?: boolean; // Strip common path prefix (default: true)
}interface DropReturn {
// State
phase: 'idle' | 'dragging' | 'processing' | 'ready' | 'error';
isProcessing: boolean;
isDragging: boolean;
isInteractive: boolean; // true when idle, dragging, or ready
hasError: boolean; // true when in error state
files: ProcessedFile[];
validFiles: ProcessedFile[];
sourceName: string;
status: { title: string; details: string; errors?: string[]; warnings?: string[] } | null;
// Prop getters
getDropzoneProps: (options?: { clickable?: boolean }) => {...};
getInputProps: () => {...};
// Actions
open: () => void; // Trigger file picker
processFiles: (files: File[]) => Promise<void>;
reset: () => void; // Clear all files and reset state
// Helpers
getFilesForUpload: () => File[]; // Get raw File objects for SDK
}// Default: clickable dropzone (click opens file picker)
<div {...drop.getDropzoneProps()}>
// Drag-only dropzone (no click behavior)
<div {...drop.getDropzoneProps({ clickable: false })}>
<button onClick={drop.open}>Select folder</button>
</div>Drop uses Ship SDK's validation automatically:
const drop = useDrop({ ship });
// Behind the scenes: ship.getLimits() → validateFiles()
// Client validation matches server limitsPass files to Ship SDK:
const files = drop.getFilesForUpload();
await ship.deployments.upload(files);The /testing subpath provides mock utilities for testing components that use useDrop:
import {
createMockDrop,
createMockDropWithSpies,
createMockProcessedFile,
} from '@shipstatic/drop/testing';import { render, screen } from '@testing-library/react';
import { createMockDrop, createMockProcessedFile } from '@shipstatic/drop/testing';
it('shows file count when ready', () => {
const drop = createMockDrop({
phase: 'ready',
files: [
createMockProcessedFile('index.html'),
createMockProcessedFile('style.css'),
],
});
render(<MyDropzone drop={drop} />);
expect(screen.getByText('2 files ready')).toBeInTheDocument();
});import userEvent from '@testing-library/user-event';
import { createMockDropWithSpies, createMockProcessedFile } from '@shipstatic/drop/testing';
it('calls reset when Clear is clicked', async () => {
const { drop, spies } = createMockDropWithSpies({
phase: 'ready',
files: [createMockProcessedFile('index.html')],
});
render(<MyDropzone drop={drop} />);
await userEvent.click(screen.getByText('Clear'));
expect(spies.reset.toHaveBeenCalled()).toBe(true);
});| Function | Purpose |
|---|---|
createMockDrop(options?) |
Mock DropReturn for rendering tests |
createMockDropWithSpies(options?) |
Mock with call tracking for interaction tests |
createMockProcessedFile(name, options?) |
Mock ProcessedFile |
createMockFile(name, content?, type?) |
Mock File object |
createMockFileWithPath(name, path, ...) |
Mock File with webkitRelativePath |
createMockErrorStatus(title?, details?, errors?) |
Mock error status |
createMockProcessingStatus(title?, details?) |
Mock processing status |
createMockReadyStatus(count) |
Mock ready status ("N file(s) are ready.") |
- React 18+ or 19+
- Modern browsers (Chrome, Edge, Safari 11.1+, Firefox 50+)
MIT