Skip to content

Commit 03d2fca

Browse files
committed
refactor: extract composable logic into focused modules and enhance docs
1 parent cb02d67 commit 03d2fca

File tree

19 files changed

+1562
-1016
lines changed

19 files changed

+1562
-1016
lines changed

docs/content/2.usage/1.overview.md

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -128,45 +128,6 @@ Clear all files and reset state.
128128
uploader.reset()
129129
```
130130

131-
### Upload Configuration
132-
133-
#### `onUpload(fn: UploadFn): void`
134-
135-
Set the upload handler function. Required unless using a storage plugin.
136-
137-
```ts
138-
uploader.onUpload(async (file, onProgress) => {
139-
const formData = new FormData()
140-
formData.append("file", file.data as Blob)
141-
142-
const xhr = new XMLHttpRequest()
143-
144-
xhr.upload.onprogress = (e) => {
145-
if (e.lengthComputable) {
146-
onProgress(Math.round((e.loaded / e.total) * 100))
147-
}
148-
}
149-
150-
return new Promise((resolve, reject) => {
151-
xhr.onload = () => resolve(JSON.parse(xhr.response))
152-
xhr.onerror = () => reject(new Error("Upload failed"))
153-
xhr.open("POST", "/api/upload")
154-
xhr.send(formData)
155-
})
156-
})
157-
```
158-
159-
#### `onGetRemoteFile(fn: GetRemoteFileFn): void`
160-
161-
Set a function to fetch remote file metadata. Used when initializing existing files.
162-
163-
```ts
164-
uploader.onGetRemoteFile(async (fileId) => {
165-
const response = await fetch(`/api/files/${fileId}`)
166-
return await response.json()
167-
})
168-
```
169-
170131
### File Access Methods
171132

172133
#### `getFile(fileId: string): UploadFile`

docs/content/2.usage/2.events.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,33 @@ uploader.on("upload:progress", ({ file, progress }) => {
107107

108108
#### `upload:complete`
109109

110-
Fired when all uploads are complete.
110+
Fired when the current upload batch completes (including any errors).
111111

112112
```ts
113113
uploader.on("upload:complete", (files: UploadFile[]) => {
114-
console.log("All uploads complete!")
114+
console.log("Batch complete:", files.length, "files processed")
115115
hideLoadingIndicator()
116+
})
117+
```
116118

117-
// Access upload results
118-
files.forEach((file) => {
119-
console.log(file.name, "->", file.remoteUrl)
120-
})
119+
#### `files:uploaded`
120+
121+
Fired when **all** files in the uploader reach `complete` status. This is the best event for final form submission.
122+
123+
```ts
124+
uploader.on("files:uploaded", (files: UploadFile[]) => {
125+
console.log("All files uploaded successfully!")
126+
127+
// Safe to submit form - all files are uploaded
128+
const urls = files.map((f) => f.remoteUrl)
129+
submitForm({ attachments: urls })
121130
})
122131
```
123132

133+
::callout{type="info"}
134+
`files:uploaded` only fires once per "session" - adding new files resets it so it can fire again.
135+
::
136+
124137
#### `upload:error`
125138

126139
Fired on upload errors.
@@ -215,6 +228,7 @@ type UploaderEvents = {
215228
"upload:complete": UploadFile[]
216229
"upload:error": FileError
217230
"upload:progress": { file: UploadFile; progress: number }
231+
"files:uploaded": UploadFile[] // All files complete
218232
"files:reorder": { oldIndex: number; newIndex: number }
219233
"initialFiles:loaded": UploadFile[]
220234
"initialFiles:error": unknown

docs/content/5.advanced/2.custom-plugins.md

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,63 @@
11
---
2-
title: Plugins
3-
description: Create your own validators and processors.
2+
title: Plugin Authoring
3+
description: Create your own validators and processors with this comprehensive guide.
44
navigation:
55
icon: i-lucide-puzzle
66
---
77

8-
# Custom Plugins
8+
# Plugin Authoring Guide
99

10-
Use `defineProcessingPlugin` to create custom validators and file processors.
10+
This guide covers everything you need to know to create custom plugins for Nuxt Upload Kit.
11+
12+
## Architecture Overview
13+
14+
Nuxt Upload Kit uses a **plugin-based architecture** with three plugin types:
15+
16+
| Type | Purpose | Example | Define With |
17+
|------|---------|---------|-------------|
18+
| **Validator** | Gate files before adding | Max file size, allowed types | `defineProcessingPlugin` |
19+
| **Processor** | Transform files | Thumbnails, compression | `defineProcessingPlugin` |
20+
| **Storage** | Handle persistence | S3, Azure, Firebase | `defineStorageAdapter` |
21+
22+
## Plugin Lifecycle
23+
24+
```
25+
┌─────────────────────────────────────────────────────────────────────┐
26+
│ ADDING FILES │
27+
│ │
28+
│ addFile() ──▶ [validate] ──▶ [preprocess] ──▶ file:added │
29+
│ │ │
30+
│ ▼ (fail) │
31+
│ file:error │
32+
└─────────────────────────────────────────────────────────────────────┘
33+
34+
│ upload() called
35+
36+
┌─────────────────────────────────────────────────────────────────────┐
37+
│ UPLOADING │
38+
│ │
39+
│ [process] ──▶ storage.upload ──▶ [complete] ──▶ upload:complete │
40+
│ │ │ │
41+
│ ▼ (fail) ▼ (fail) │
42+
│ file:error file:error │
43+
└─────────────────────────────────────────────────────────────────────┘
44+
45+
Legend: [hooks] are plugin extension points
46+
```
47+
48+
### When Each Hook Runs
49+
50+
| Hook | When | Use For |
51+
|------|------|---------|
52+
| `validate` | Immediately on `addFile()` | Rejecting invalid files |
53+
| `preprocess` | After validation passes | Generating thumbnails, previews |
54+
| `process` | When `upload()` is called | Compression, transformations |
55+
| `complete` | After successful upload | Post-upload cleanup |
1156

1257
## Basic Structure
1358

59+
Use `defineProcessingPlugin` to create custom validators and file processors.
60+
1461
```ts
1562
import { defineProcessingPlugin } from "nuxt-upload-kit"
1663

@@ -292,3 +339,103 @@ export const PluginImageResizer = defineProcessingPlugin<ImageResizerOptions, Im
292339
}
293340
})
294341
````
342+
343+
## Best Practices
344+
345+
### 1. Always Check File Source
346+
347+
Remote files (`source: "storage"`) have `data: null`. Always check before accessing file data:
348+
349+
```ts
350+
process: async (file, context) => {
351+
if (file.source !== "local") {
352+
context.emit("skip", { file, reason: "Remote file" })
353+
return file
354+
}
355+
356+
// Safe to access file.data
357+
const blob = file.data
358+
}
359+
```
360+
361+
### 2. Return Original on Failure
362+
363+
Never throw from `process` hooks unless you want to fail the upload. Return the original file to continue:
364+
365+
```ts
366+
process: async (file, context) => {
367+
try {
368+
// Transform file...
369+
return transformedFile
370+
} catch (error) {
371+
console.warn(`[MyPlugin] Failed:`, error)
372+
return file // Continue with original
373+
}
374+
}
375+
```
376+
377+
### 3. Use Namespaced Plugin IDs
378+
379+
Prefix your plugin IDs to avoid collisions:
380+
381+
```ts
382+
// Good
383+
id: "mycompany-watermark"
384+
id: "acme-validator-dimensions"
385+
386+
// Avoid
387+
id: "watermark" // Too generic
388+
```
389+
390+
### 4. Emit Events for Observability
391+
392+
Emit events so users can track plugin activity:
393+
394+
```ts
395+
process: async (file, context) => {
396+
context.emit("start", { file })
397+
398+
// Do work...
399+
400+
context.emit("complete", { file, savings: originalSize - newSize })
401+
return file
402+
}
403+
```
404+
405+
### 5. Document Your Plugin
406+
407+
Include JSDoc comments with examples:
408+
409+
```ts
410+
/**
411+
* Validates that images meet minimum dimension requirements.
412+
*
413+
* @example
414+
* ```ts
415+
* useUploadKit({
416+
* plugins: [
417+
* ValidatorMinDimensions({ minWidth: 800, minHeight: 600 })
418+
* ]
419+
* })
420+
* ```
421+
*/
422+
export const ValidatorMinDimensions = defineProcessingPlugin<Options>((options) => ({
423+
// ...
424+
}))
425+
```
426+
427+
### 6. Handle All Image Types Appropriately
428+
429+
Some formats need special handling:
430+
431+
```ts
432+
process: async (file, context) => {
433+
// Skip non-processable formats
434+
if (file.mimeType === "image/gif" || file.mimeType === "image/svg+xml") {
435+
context.emit("skip", { file, reason: "Format not supported" })
436+
return file
437+
}
438+
439+
// Process other images...
440+
}
441+
```

docs/content/5.advanced/3.custom-storage-adapters.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ export const PluginMyStorage = defineStorageAdapter<MyStorageOptions, MyStorageR
101101

102102
// Optional: Delete a file
103103
remove: async (file, context) => {
104-
await fetch(`${options.apiUrl}/${file.id}`, {
104+
// Use storageKey for deletion (set after upload or from initialFiles)
105+
if (!file.storageKey) return
106+
107+
await fetch(`${options.apiUrl}/${file.storageKey}`, {
105108
method: "DELETE",
106109
headers: { "X-API-Key": options.apiKey },
107110
})
@@ -153,18 +156,60 @@ The `upload` hook **must** return an object containing at least a `url` property
153156
// Minimum required
154157
return { url: "https://storage.example.com/file.jpg" }
155158

156-
// With additional metadata
159+
// With additional metadata (recommended)
157160
return {
158161
url: "https://storage.example.com/file.jpg",
159-
id: "file-123",
162+
storageKey: "uploads/user-123/file.jpg", // Full path for retrieval/deletion
160163
etag: "abc123",
161164
bucket: "my-bucket",
162-
// Any other properties you need
163165
}
164166
```
165167

166168
The returned object becomes available as `file.uploadResult` after the upload completes.
167169

170+
## The storageKey Pattern
171+
172+
The `storageKey` is the **full path** used to identify files in storage. It enables:
173+
- Loading existing files via `initialFiles`
174+
- Deleting files with `removeFile()`
175+
- Round-trip consistency (upload → store key → retrieve later)
176+
177+
```ts
178+
// Upload returns storageKey
179+
upload: async (file, context) => {
180+
const path = `uploads/${options.folder}/${file.id}`
181+
182+
await uploadToStorage(path, file.data)
183+
184+
return {
185+
url: `https://cdn.example.com/${path}`,
186+
storageKey: path, // Save this to your database
187+
}
188+
}
189+
190+
// getRemoteFile receives storageKey
191+
getRemoteFile: async (storageKey, context) => {
192+
const metadata = await getFromStorage(storageKey)
193+
194+
return {
195+
size: metadata.size,
196+
mimeType: metadata.contentType,
197+
remoteUrl: `https://cdn.example.com/${storageKey}`,
198+
}
199+
}
200+
201+
// remove receives file with storageKey
202+
remove: async (file, context) => {
203+
if (!file.storageKey) return // Not uploaded yet
204+
205+
await deleteFromStorage(file.storageKey)
206+
}
207+
```
208+
209+
::callout{type="info"}
210+
Store the `storageKey` in your database alongside other file metadata. Pass it back via `initialFiles` to reload files.
211+
::
212+
168213
## Remote File Metadata
169214

170215
The `getRemoteFile` hook must return an object with these properties:

0 commit comments

Comments
 (0)