nuxt-upload-kit 0.1.23 → 0.1.25

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-upload-kit",
3
3
  "configKey": "uploadKit",
4
- "version": "0.1.23",
4
+ "version": "0.1.25",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -117,7 +117,7 @@ export function createFileOperations(deps) {
117
117
  const storagePlugin = getStoragePlugin();
118
118
  if (storagePlugin?.hooks.remove) {
119
119
  try {
120
- const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
120
+ const context = createPluginContext(storagePlugin.id, files.value, options, emitter, storagePlugin);
121
121
  await storagePlugin.hooks.remove(file, context);
122
122
  } catch (error) {
123
123
  console.error(`Storage plugin remove error:`, error);
@@ -71,6 +71,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
71
71
  readonly meta: {
72
72
  readonly [x: string]: Readonly<unknown>;
73
73
  };
74
+ readonly thumbnail?: {
75
+ readonly url: string;
76
+ readonly storageKey: string;
77
+ } | undefined;
74
78
  } | {
75
79
  readonly source: Exclude<import("./types.js").FileSource, "local">;
76
80
  readonly data: null;
@@ -93,6 +97,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
93
97
  readonly meta: {
94
98
  readonly [x: string]: Readonly<unknown>;
95
99
  };
100
+ readonly thumbnail?: {
101
+ readonly url: string;
102
+ readonly storageKey: string;
103
+ } | undefined;
96
104
  })[], readonly ({
97
105
  readonly source: "local";
98
106
  readonly data: {
@@ -164,6 +172,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
164
172
  readonly meta: {
165
173
  readonly [x: string]: Readonly<unknown>;
166
174
  };
175
+ readonly thumbnail?: {
176
+ readonly url: string;
177
+ readonly storageKey: string;
178
+ } | undefined;
167
179
  } | {
168
180
  readonly source: Exclude<import("./types.js").FileSource, "local">;
169
181
  readonly data: null;
@@ -186,6 +198,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
186
198
  readonly meta: {
187
199
  readonly [x: string]: Readonly<unknown>;
188
200
  };
201
+ readonly thumbnail?: {
202
+ readonly url: string;
203
+ readonly storageKey: string;
204
+ } | undefined;
189
205
  })[]>>;
190
206
  totalProgress: import("vue").ComputedRef<number>;
191
207
  isReady: Readonly<import("vue").Ref<boolean, boolean>>;
@@ -48,7 +48,8 @@ export const useUploadKit = (_options = {}) => {
48
48
  PluginThumbnailGenerator({
49
49
  maxWidth: thumbOpts.width ?? 128,
50
50
  maxHeight: thumbOpts.height ?? 128,
51
- quality: thumbOpts.quality ?? 1
51
+ quality: thumbOpts.quality ?? 1,
52
+ upload: thumbOpts.upload ?? false
52
53
  })
53
54
  );
54
55
  }
@@ -65,7 +66,7 @@ export const useUploadKit = (_options = {}) => {
65
66
  })
66
67
  );
67
68
  }
68
- const { getPluginEmitFn, runPluginStage } = createPluginRunner({ options, files, emitter });
69
+ const { getPluginEmitFn, runPluginStage } = createPluginRunner({ options, files, emitter, getStoragePlugin });
69
70
  const uploadHolder = { fn: async () => {
70
71
  } };
71
72
  const fileOps = createFileOperations({
@@ -100,7 +101,7 @@ export const useUploadKit = (_options = {}) => {
100
101
  if (!storagePlugin?.hooks.getRemoteFile) {
101
102
  throw new Error("Storage plugin with getRemoteFile hook is required to initialize existing files");
102
103
  }
103
- const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
104
+ const context = createPluginContext(storagePlugin.id, files.value, options, emitter, storagePlugin);
104
105
  const remoteFileData = await storagePlugin.hooks.getRemoteFile(storageKey, context);
105
106
  const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
106
107
  const name = storageKey.split("/").pop() || storageKey;
@@ -205,7 +206,8 @@ export const useUploadKit = (_options = {}) => {
205
206
  const currentFile = files.value.find((f) => f.id === processedFile.id);
206
207
  const preview = currentFile?.preview || remoteUrl;
207
208
  const storageKey = extractStorageKey(uploadResult);
208
- updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview, storageKey });
209
+ const thumbnail = currentFile?.thumbnail;
210
+ updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview, storageKey, thumbnail });
209
211
  };
210
212
  const upload = async () => {
211
213
  const filesToUpload = files.value.filter((f) => f.status === "waiting");
@@ -1,11 +1,12 @@
1
1
  import type { Ref } from "vue";
2
2
  import type { Emitter } from "mitt";
3
- import type { UploadFile, UploadOptions, PluginLifecycleStage } from "./types.js";
3
+ import type { UploadFile, UploadOptions, PluginLifecycleStage, StoragePlugin } from "./types.js";
4
4
  type EmitFn = <K extends string | number | symbol>(event: K, payload: any) => void;
5
5
  export interface PluginRunnerDeps<TUploadResult = any> {
6
6
  options: UploadOptions;
7
7
  files: Ref<UploadFile<TUploadResult>[]>;
8
8
  emitter: Emitter<any>;
9
+ getStoragePlugin: () => StoragePlugin<any, any> | null;
9
10
  }
10
11
  /**
11
12
  * Creates the plugin execution system with cached emit functions
@@ -1,5 +1,5 @@
1
1
  export function createPluginRunner(deps) {
2
- const { options, files, emitter } = deps;
2
+ const { options, files, emitter, getStoragePlugin } = deps;
3
3
  const pluginEmitFunctions = /* @__PURE__ */ new Map();
4
4
  const getPluginEmitFn = (pluginId) => {
5
5
  let emitFn = pluginEmitFunctions.get(pluginId);
@@ -34,9 +34,11 @@ export function createPluginRunner(deps) {
34
34
  const hook = plugin.hooks[stage];
35
35
  if (!hook) continue;
36
36
  try {
37
+ const storage = getStoragePlugin();
37
38
  const context = {
38
39
  files: files.value,
39
40
  options,
41
+ storage: storage || void 0,
40
42
  emit: getPluginEmitFn(plugin.id)
41
43
  };
42
44
  const result = await callPluginHook(hook, stage, currentFile, context);
@@ -97,6 +97,26 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
97
97
  };
98
98
  return {
99
99
  id: "azure-datalake-storage",
100
+ async upload(data, storageKey, uploadOptions) {
101
+ const requestKey = buildFullStorageKey(storageKey, true);
102
+ const fileClient = await getFileClient(requestKey, "upload");
103
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
104
+ await fileClient.upload(data, {
105
+ pathHttpHeaders: {
106
+ ...options.pathHttpHeaders,
107
+ contentType
108
+ },
109
+ onProgress: uploadOptions?.onProgress ? ({ loadedBytes }) => {
110
+ const percentage = Math.round(loadedBytes / data.size * 100);
111
+ uploadOptions.onProgress(percentage);
112
+ } : void 0
113
+ });
114
+ const actualStorageKey = getBlobPathFromUrl(fileClient.url) || requestKey;
115
+ return {
116
+ url: fileClient.url,
117
+ storageKey: actualStorageKey
118
+ };
119
+ },
100
120
  hooks: {
101
121
  /**
102
122
  * Upload file to Azure Blob Storage
@@ -56,6 +56,12 @@ export const PluginFirebaseStorage = defineStorageAdapter((options) => {
56
56
  };
57
57
  return {
58
58
  id: "firebase-storage",
59
+ async upload(data, storageKey, uploadOptions) {
60
+ const fullKey = buildFullStorageKey(storageKey);
61
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
62
+ return uploadToFirebase(fullKey, data, contentType, storageKey, uploadOptions?.onProgress || (() => {
63
+ }));
64
+ },
59
65
  hooks: {
60
66
  /**
61
67
  * Upload file to Firebase Storage
@@ -33,6 +33,23 @@ export const PluginS3 = defineStorageAdapter((options) => {
33
33
  }
34
34
  return {
35
35
  id: "s3-storage",
36
+ async upload(data, storageKey, uploadOptions) {
37
+ return withRetry(async () => {
38
+ const fullKey = buildFullStorageKey(storageKey);
39
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
40
+ const { uploadUrl, publicUrl } = await options.getPresignedUploadUrl(fullKey, contentType, {
41
+ fileName: storageKey,
42
+ fileSize: data.size
43
+ });
44
+ const etag = await uploadWithProgress(uploadUrl, data, contentType, uploadOptions?.onProgress || (() => {
45
+ }));
46
+ return {
47
+ url: publicUrl,
48
+ storageKey: fullKey,
49
+ etag
50
+ };
51
+ }, `Standalone upload "${storageKey}"`);
52
+ },
36
53
  hooks: {
37
54
  /**
38
55
  * Upload file to S3 using presigned URL
@@ -3,6 +3,7 @@ interface ThumbnailGeneratorOptions {
3
3
  maxHeight?: number;
4
4
  quality?: number;
5
5
  videoCaptureTime?: number;
6
+ upload?: boolean;
6
7
  }
7
8
  export declare const PluginThumbnailGenerator: (options: ThumbnailGeneratorOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
8
9
  export {};
@@ -1,5 +1,5 @@
1
1
  import { defineProcessingPlugin } from "../types.js";
2
- import { calculateThumbnailDimensions } from "../utils.js";
2
+ import { calculateThumbnailDimensions, dataUrlToBlob, deriveThumbnailKey } from "../utils.js";
3
3
  export const PluginThumbnailGenerator = defineProcessingPlugin((pluginOptions) => {
4
4
  return {
5
5
  id: "thumbnail-generator",
@@ -35,6 +35,24 @@ export const PluginThumbnailGenerator = defineProcessingPlugin((pluginOptions) =
35
35
  URL.revokeObjectURL(sourceUrl);
36
36
  }
37
37
  return file;
38
+ },
39
+ process: async (file, context) => {
40
+ const { upload = false } = pluginOptions;
41
+ if (upload && context.storage && file.preview?.startsWith("data:")) {
42
+ try {
43
+ const thumbnailBlob = dataUrlToBlob(file.preview);
44
+ const thumbnailKey = deriveThumbnailKey(file.id);
45
+ const thumbnailResult = await context.storage.upload(thumbnailBlob, thumbnailKey, {
46
+ contentType: thumbnailBlob.type
47
+ });
48
+ file.thumbnail = { url: thumbnailResult.url, storageKey: thumbnailResult.storageKey };
49
+ } catch (err) {
50
+ if (import.meta.dev) {
51
+ console.warn(`[thumbnail-generator] Thumbnail upload failed for "${file.name}":`, err);
52
+ }
53
+ }
54
+ }
55
+ return file;
38
56
  }
39
57
  }
40
58
  };
@@ -81,6 +81,14 @@ export interface BaseUploadFile<TUploadResult = any> {
81
81
  * Plugins can add data here (e.g., { extension: 'jpg', originalWidth: 4000 })
82
82
  */
83
83
  meta: Record<string, unknown>;
84
+ /**
85
+ * Thumbnail upload result (populated when `thumbnails.upload` is enabled).
86
+ * Contains the URL and storage key for the uploaded thumbnail.
87
+ */
88
+ thumbnail?: {
89
+ url: string;
90
+ storageKey: string;
91
+ };
84
92
  }
85
93
  /**
86
94
  * Local upload file - originates from user's device
@@ -272,6 +280,26 @@ export interface ThumbnailOptions {
272
280
  width?: number;
273
281
  height?: number;
274
282
  quality?: number;
283
+ /**
284
+ * Upload generated thumbnails to storage alongside main files.
285
+ * Requires a storage adapter with standalone upload support.
286
+ *
287
+ * When enabled, after each file uploads, its thumbnail preview
288
+ * is converted to a Blob and uploaded using `storage.upload()`.
289
+ * The result is stored on `file.thumbnail`.
290
+ *
291
+ * @default false
292
+ */
293
+ upload?: boolean;
294
+ }
295
+ /**
296
+ * Options for standalone upload (bypassing the useUploadKit pipeline)
297
+ */
298
+ export interface StandaloneUploadOptions {
299
+ /** MIME type of the data being uploaded. Defaults to 'application/octet-stream'. */
300
+ contentType?: string;
301
+ /** Progress callback (0-100) */
302
+ onProgress?: (percentage: number) => void;
275
303
  }
276
304
  export interface ImageCompressionOptions {
277
305
  maxWidth?: number;
@@ -316,6 +344,11 @@ export type UploaderEvents<TUploadResult = any> = CoreUploaderEvents<TUploadResu
316
344
  export type PluginContext<TPluginEvents extends Record<string, any> = Record<string, never>> = {
317
345
  files: UploadFile[];
318
346
  options: UploadOptions;
347
+ /**
348
+ * Storage plugin for uploading derivatives (thumbnails, variants, etc.)
349
+ * Available only when a storage plugin is configured
350
+ */
351
+ storage?: StoragePlugin<any, any>;
319
352
  /**
320
353
  * Emit custom plugin events
321
354
  * Events are automatically prefixed with the plugin ID
@@ -405,6 +438,39 @@ export interface StoragePlugin<TUploadResult = any, TPluginEvents extends Record
405
438
  hooks: StoragePluginHooks<TUploadResult, TPluginEvents>;
406
439
  options?: UploadOptions;
407
440
  events?: TPluginEvents;
441
+ /**
442
+ * Upload a raw Blob or File directly to storage, bypassing the useUploadKit pipeline.
443
+ *
444
+ * This is a low-level primitive for uploading arbitrary data without going through
445
+ * validation, preprocessing, or processing stages. Useful for:
446
+ * - Uploading edited/cropped images
447
+ * - Uploading thumbnails separately
448
+ * - Any scenario where you have a Blob and just need it in storage
449
+ *
450
+ * The `storageKey` is treated like a filename — the adapter prepends any configured
451
+ * path prefix (e.g., `options.path`), and the server-side SAS/presigned URL handler
452
+ * may further resolve it (e.g., prepend organization ID).
453
+ *
454
+ * @param data - The Blob or File to upload
455
+ * @param storageKey - Filename or relative path for the file in storage
456
+ * @param options - Optional content type and progress callback
457
+ * @returns The upload result with `url` and resolved `storageKey`
458
+ *
459
+ * @example
460
+ * ```typescript
461
+ * const storage = PluginAzureDataLake({ getSASUrl: ... })
462
+ *
463
+ * // Upload an edited image
464
+ * const result = await storage.upload(croppedBlob, 'edited_photo.jpg', {
465
+ * contentType: 'image/jpeg',
466
+ * })
467
+ * console.log(result.storageKey) // Full resolved path in storage
468
+ * ```
469
+ */
470
+ upload: (data: Blob | File, storageKey: string, options?: StandaloneUploadOptions) => Promise<TUploadResult & {
471
+ url: string;
472
+ storageKey: string;
473
+ }>;
408
474
  }
409
475
  /**
410
476
  * Base plugin interface (for internal use - supports both types)
@@ -1,4 +1,4 @@
1
- import type { PluginContext, UploadFile, FileError, UploadOptions, InitialFileInput } from "./types.js";
1
+ import type { PluginContext, UploadFile, FileError, UploadOptions, InitialFileInput, StoragePlugin } from "./types.js";
2
2
  import type { Emitter } from "mitt";
3
3
  /**
4
4
  * Get file extension from filename
@@ -7,7 +7,7 @@ export declare function getExtension(fullFileName: string): string;
7
7
  /**
8
8
  * Create a plugin context object with consistent structure
9
9
  */
10
- export declare function createPluginContext<TPluginEvents extends Record<string, any> = Record<string, never>>(pluginId: string, files: UploadFile[], options: UploadOptions, emitter: Emitter<any>): PluginContext<TPluginEvents>;
10
+ export declare function createPluginContext<TPluginEvents extends Record<string, any> = Record<string, never>>(pluginId: string, files: UploadFile[], options: UploadOptions, emitter: Emitter<any>, storage?: StoragePlugin<any, any>): PluginContext<TPluginEvents>;
11
11
  /**
12
12
  * Create a consistent file error object
13
13
  */
@@ -19,6 +19,16 @@ export declare function calculateThumbnailDimensions(originalWidth: number, orig
19
19
  width: number;
20
20
  height: number;
21
21
  };
22
+ /**
23
+ * Convert a base64-encoded data URL (e.g., from canvas.toDataURL) to a Blob.
24
+ * Only supports base64-encoded data URLs (`;base64,` format).
25
+ */
26
+ export declare function dataUrlToBlob(dataUrl: string): Blob;
27
+ /**
28
+ * Derive a thumbnail storage key from a file ID by appending '_thumb' before the extension
29
+ * @example "1738345678901-abc123.jpg" → "1738345678901-abc123_thumb.jpg"
30
+ */
31
+ export declare function deriveThumbnailKey(fileId: string): string;
22
32
  /**
23
33
  * Cleanup object URLs to prevent memory leaks
24
34
  * @param urlMap Map of file IDs to object URLs
@@ -6,10 +6,11 @@ export function getExtension(fullFileName) {
6
6
  }
7
7
  return fullFileName.slice(lastDot + 1).toLocaleLowerCase();
8
8
  }
9
- export function createPluginContext(pluginId, files, options, emitter) {
9
+ export function createPluginContext(pluginId, files, options, emitter, storage) {
10
10
  return {
11
11
  files,
12
12
  options,
13
+ storage,
13
14
  emit: (event, payload) => {
14
15
  const prefixedEvent = `${pluginId}:${String(event)}`;
15
16
  emitter.emit(prefixedEvent, payload);
@@ -37,6 +38,24 @@ export function calculateThumbnailDimensions(originalWidth, originalHeight, maxW
37
38
  }
38
39
  return { width, height };
39
40
  }
41
+ export function dataUrlToBlob(dataUrl) {
42
+ if (!dataUrl.includes(";base64,")) {
43
+ throw new Error("dataUrlToBlob only supports base64-encoded data URLs");
44
+ }
45
+ const [header, base64] = dataUrl.split(",");
46
+ const mimeType = header.match(/:(.*?);/)?.[1] ?? "image/jpeg";
47
+ const binary = atob(base64);
48
+ const bytes = new Uint8Array(binary.length);
49
+ for (let i = 0; i < binary.length; i++) {
50
+ bytes[i] = binary.charCodeAt(i);
51
+ }
52
+ return new Blob([bytes], { type: mimeType });
53
+ }
54
+ export function deriveThumbnailKey(fileId) {
55
+ const lastDot = fileId.lastIndexOf(".");
56
+ if (lastDot === -1) return `${fileId}_thumb`;
57
+ return `${fileId.slice(0, lastDot)}_thumb${fileId.slice(lastDot)}`;
58
+ }
40
59
  export function cleanupObjectURLs(urlMap, fileId) {
41
60
  if (fileId) {
42
61
  const url = urlMap.get(fileId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-upload-kit",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "A powerful, plugin-based file upload manager for Nuxt applications",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/genu/nuxt-upload-kit.git",
@@ -50,20 +50,20 @@
50
50
  "docs:dev": "nuxt dev docs --extends docus"
51
51
  },
52
52
  "dependencies": {
53
- "mitt": "^3.0.1"
53
+ "mitt": "3.0.1"
54
54
  },
55
55
  "devDependencies": {
56
- "@aws-sdk/client-s3": "3.985.0",
57
- "@aws-sdk/lib-storage": "3.985.0",
58
- "@azure/storage-file-datalake": "^12.29.0",
56
+ "@aws-sdk/client-s3": "3.989.0",
57
+ "@aws-sdk/lib-storage": "3.989.0",
58
+ "@azure/storage-file-datalake": "12.29.0",
59
59
  "@ffmpeg/ffmpeg": "0.12.15",
60
60
  "@ffmpeg/util": "0.12.2",
61
- "@nuxt/devtools": "^3.1.1",
62
- "@nuxt/eslint-config": "^1.13.0",
61
+ "@nuxt/devtools": "3.2.1",
62
+ "@nuxt/eslint-config": "1.15.1",
63
63
  "@nuxt/kit": "4.3.1",
64
- "@nuxt/module-builder": "^1.0.2",
64
+ "@nuxt/module-builder": "1.0.2",
65
65
  "@nuxt/schema": "4.3.1",
66
- "@nuxt/test-utils": "^4.0.0",
66
+ "@nuxt/test-utils": "4.0.0",
67
67
  "@types/node": "latest",
68
68
  "@vitejs/plugin-vue": "6.0.4",
69
69
  "@vitest/coverage-v8": "4.0.18",
@@ -71,7 +71,7 @@
71
71
  "eslint-config-prettier": "10.1.8",
72
72
  "eslint-plugin-prettier": "5.5.5",
73
73
  "firebase": "12.9.0",
74
- "happy-dom": "20.5.3",
74
+ "happy-dom": "20.6.1",
75
75
  "nuxt": "4.3.1",
76
76
  "prettier": "3.8.1",
77
77
  "typescript": "5.9.3",
@@ -100,5 +100,5 @@
100
100
  "optional": true
101
101
  }
102
102
  },
103
- "packageManager": "pnpm@10.29.2"
103
+ "packageManager": "pnpm@10.29.3"
104
104
  }