nuxt-filer 0.0.8 → 0.0.11

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
@@ -16,6 +16,7 @@ File storage module for Nuxt. Provides a server-side `useFileStorage()` composab
16
16
  - **Auto-imported** — `useFileStorage()`, types, and provider utilities are auto-imported in server context
17
17
  - **Group-based organization** — files are organized by `groupId` (project, ticket, order, etc.)
18
18
  - **`@nuxt/image` integration** — when `@nuxt/image` is installed, an IPX endpoint is wired up automatically so `<NuxtImg provider="filer" src="<groupId>/<id>" />` returns optimized variants of stored files
19
+ - **Upload-time image processing** — optionally run images through Sharp when storing them (resize, format-convert, optimize, preserve animation) via a per-call `transform` option or the standalone `transformImage()` util
19
20
 
20
21
  ## Quick Setup
21
22
 
@@ -76,11 +77,49 @@ export default defineEventHandler(async (event) => {
76
77
  })
77
78
  ```
78
79
 
80
+ ### Upload-time image processing
81
+
82
+ Pass a `transform` option to `upload()` to process an image **before it is stored** — useful for normalizing user uploads or rehosted remote images to a capped size and compact format. The processed bytes are what gets stored, and the file's `meta.mime` / `meta.width` / `meta.height` are updated to match the result.
83
+
84
+ ```ts
85
+ const id = await storage.upload(groupId, file.data, {
86
+ meta: { name: file.filename!, mime: file.type!, type: 'image', version: 1 },
87
+ transform: {
88
+ width: 128,
89
+ height: 128,
90
+ format: 'webp', // convert to webp
91
+ // fit: 'inside' (default), withoutEnlargement: true (default),
92
+ // quality, background, animated (default true)
93
+ },
94
+ })
95
+ ```
96
+
97
+ You can also call the util directly (e.g. for images fetched server-side):
98
+
99
+ ```ts
100
+ const res = await transformImage(buffer, { width: 64, format: 'webp' })
101
+ // res: { data: Buffer, mime: 'image/webp', format: 'webp', width: 64, height: 64 }
102
+ ```
103
+
104
+ **`transform` / `transformImage()` options**
105
+
106
+ | Option | Description |
107
+ |---|---|
108
+ | `width`, `height` | Target box in px (combined with `fit`) |
109
+ | `fit` | Resize fit mode (`inside` default, or `cover`/`contain`/`fill`/`outside`) |
110
+ | `withoutEnlargement` | Never scale up beyond the original (default `true`) |
111
+ | `format` | Output format: `webp` / `png` / `jpeg` / `avif` / `gif` (default: keep input) |
112
+ | `quality` | Output quality `1-100` for lossy formats |
113
+ | `animated` | Preserve all frames of animated inputs (default `true`; only retained when `format` is `webp`/`gif`) |
114
+ | `background` | Background used when flattening transparency |
115
+
116
+ > Image processing requires the optional [`sharp`](https://sharp.pixelplumbing.com/) peer dependency. Install it (`npm i sharp`) only if you use `transform` / `transformImage()` — calling them without `sharp` throws a clear error. Without a `transform`, `upload()` stores the raw bytes unchanged and needs no extra dependency.
117
+
79
118
  ### `useFileStorage()` API
80
119
 
81
120
  | Method | Description |
82
121
  |---|---|
83
- | `upload(groupId, data, options?)` | Store a file, returns its ID |
122
+ | `upload(groupId, data, options?)` | Store a file, returns its ID. `options.transform` runs the bytes through Sharp first (see above) |
84
123
  | `list(groupId)` | List all files in a group |
85
124
  | `get(groupId, id)` | Get a file with data and metadata |
86
125
  | `getData(groupId, id)` | Get raw binary data only |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
3
  "configKey": "filer",
4
- "version": "0.0.8",
4
+ "version": "0.0.11",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -18,6 +18,10 @@ const module = defineNuxtModule({
18
18
  {
19
19
  name: "useFileStorage",
20
20
  from: resolver.resolve("./runtime/server/utils/storage")
21
+ },
22
+ {
23
+ name: "transformImage",
24
+ from: resolver.resolve("./runtime/server/utils/image")
21
25
  }
22
26
  ]);
23
27
  const providerSpecifier = resolver.resolve("./runtime/server/provider");
@@ -41,7 +45,10 @@ const module = defineNuxtModule({
41
45
  name: "FileStorageExternalProvider",
42
46
  from: typesSpecifier,
43
47
  type: true
44
- }
48
+ },
49
+ { name: "ImageFormat", from: typesSpecifier, type: true },
50
+ { name: "ImageTransformOptions", from: typesSpecifier, type: true },
51
+ { name: "ImageTransformResult", from: typesSpecifier, type: true }
45
52
  ]);
46
53
  const imageOpt = typeof options.image === "object" && options.image !== null ? options.image : {};
47
54
  const imageEnabled = options.image !== false && (imageOpt.enabled ?? true) !== false;
@@ -1,6 +1,3 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | void | Buffer<ArrayBufferLike> | {
2
- error: {
3
- message: string;
4
- };
5
- }>>;
1
+ import { type EventHandler } from 'h3';
2
+ declare const _default: EventHandler<import("h3").EventHandlerRequest, any>;
6
3
  export default _default;
@@ -1,4 +1,4 @@
1
- import { defineEventHandler } from "h3";
1
+ import { defineEventHandler, useBase } from "h3";
2
2
  import { createIPX, createIPXH3Handler } from "ipx";
3
3
  import { ipxRoute } from "#nuxt-filer-image";
4
4
  import { useFileStorageProvider } from "../provider.js";
@@ -36,14 +36,8 @@ let _ipx = null;
36
36
  function getHandler() {
37
37
  if (!_handler) {
38
38
  _ipx = createIPX({ storage: filerStorage });
39
- _handler = createIPXH3Handler(_ipx);
39
+ _handler = useBase(ipxRoute, createIPXH3Handler(_ipx));
40
40
  }
41
41
  return _handler;
42
42
  }
43
- export default defineEventHandler((event) => {
44
- const original = event.path;
45
- let sub = original.startsWith(ipxRoute) ? original.slice(ipxRoute.length) : original;
46
- if (!sub.startsWith("/")) sub = `/${sub}`;
47
- event.path = sub;
48
- return getHandler()(event);
49
- });
43
+ export default defineEventHandler((event) => getHandler()(event));
@@ -0,0 +1,11 @@
1
+ import type { ImageFormat, ImageTransformOptions, ImageTransformResult } from '../../../runtime/types.js';
2
+ export type { ImageFormat, ImageTransformOptions, ImageTransformResult };
3
+ /**
4
+ * Process an image buffer with sharp: resize within a box, convert format,
5
+ * optimize, and (by default) preserve animation for multi-frame inputs. Used
6
+ * for upload-time normalization — see `useFileStorage().upload({ transform })`.
7
+ *
8
+ * Requires the optional `sharp` peer dependency; throws a clear error if it is
9
+ * not installed.
10
+ */
11
+ export declare function transformImage(data: Buffer | Uint8Array, options?: ImageTransformOptions): Promise<ImageTransformResult>;
@@ -0,0 +1,50 @@
1
+ const MIME_BY_FORMAT = {
2
+ webp: "image/webp",
3
+ png: "image/png",
4
+ jpeg: "image/jpeg",
5
+ jpg: "image/jpeg",
6
+ avif: "image/avif",
7
+ gif: "image/gif"
8
+ };
9
+ let sharpModule;
10
+ async function loadSharp() {
11
+ if (sharpModule) return sharpModule;
12
+ try {
13
+ sharpModule = (await import("sharp")).default;
14
+ } catch {
15
+ throw new Error(
16
+ "[nuxt-filer] Image transforms require the optional peer dependency `sharp`. Install it with `npm i sharp`."
17
+ );
18
+ }
19
+ return sharpModule;
20
+ }
21
+ export async function transformImage(data, options = {}) {
22
+ const sharp = await loadSharp();
23
+ const animated = options.animated ?? true;
24
+ let pipeline = sharp(data, { animated });
25
+ if (options.width != null || options.height != null) {
26
+ pipeline = pipeline.resize({
27
+ width: options.width,
28
+ height: options.height,
29
+ fit: options.fit ?? "inside",
30
+ withoutEnlargement: options.withoutEnlargement ?? true,
31
+ background: options.background
32
+ });
33
+ }
34
+ if (options.format) {
35
+ const formatOptions = options.quality != null ? { quality: options.quality } : {};
36
+ pipeline = pipeline.toFormat(
37
+ options.format,
38
+ formatOptions
39
+ );
40
+ }
41
+ const out = await pipeline.toBuffer({ resolveWithObject: true });
42
+ const format = out.info.format;
43
+ return {
44
+ data: out.data,
45
+ format,
46
+ mime: MIME_BY_FORMAT[format] ?? `image/${format}`,
47
+ width: out.info.width,
48
+ height: out.info.height
49
+ };
50
+ }
@@ -1,8 +1,9 @@
1
- import type { FileMeta, StoredFile, ExternalRef, FileStorageProvider } from '../../../runtime/types.js';
2
- export type { FileMeta, StoredFile, ExternalRef, FileStorageProvider };
1
+ import type { FileMeta, StoredFile, ExternalRef, FileStorageProvider, ImageTransformOptions, ImageTransformResult } from '../../../runtime/types.js';
2
+ export type { FileMeta, StoredFile, ExternalRef, FileStorageProvider, ImageTransformOptions, ImageTransformResult, };
3
3
  export declare const useFileStorage: () => {
4
4
  upload: (groupId: string, data: Buffer | Uint8Array, options?: {
5
5
  meta?: FileMeta;
6
+ transform?: ImageTransformOptions;
6
7
  }) => Promise<string>;
7
8
  list: (groupId: string) => Promise<StoredFile[]>;
8
9
  get: (groupId: string, id: string) => Promise<StoredFile | null>;
@@ -1,9 +1,20 @@
1
1
  import { defu } from "defu";
2
2
  import { useFileStorageProvider } from "../provider.js";
3
+ import { transformImage } from "./image.js";
3
4
  export const useFileStorage = () => {
4
5
  const provider = useFileStorageProvider();
5
6
  async function upload(groupId, data, options = {}) {
6
- const { id } = await provider.create(groupId, data, options.meta);
7
+ let payload = data;
8
+ if (options.transform) {
9
+ const result = await transformImage(data, options.transform);
10
+ payload = result.data;
11
+ if (options.meta) {
12
+ options.meta.mime = result.mime;
13
+ options.meta.width = result.width;
14
+ options.meta.height = result.height;
15
+ }
16
+ }
17
+ const { id } = await provider.create(groupId, payload, options.meta);
7
18
  return id;
8
19
  }
9
20
  async function list(groupId) {
@@ -7,6 +7,51 @@ export interface FileMeta {
7
7
  comment?: string;
8
8
  [key: string]: unknown;
9
9
  }
10
+ /** Output image format for {@link transformImage}. */
11
+ export type ImageFormat = 'webp' | 'png' | 'jpeg' | 'avif' | 'gif';
12
+ /**
13
+ * Options for upload-time image processing, backed by the optional `sharp`
14
+ * peer dependency. Passed via `useFileStorage().upload(.., { transform })` or
15
+ * to the standalone `transformImage()` util.
16
+ */
17
+ export interface ImageTransformOptions {
18
+ /** Target width in px. Combined with `fit` to bound the image. */
19
+ width?: number;
20
+ /** Target height in px. Combined with `fit` to bound the image. */
21
+ height?: number;
22
+ /**
23
+ * How the image is resized to fit `width`/`height`. Mirrors sharp's `fit`.
24
+ * Default: `'inside'` (preserve aspect ratio, fit within the box).
25
+ */
26
+ fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
27
+ /** Never scale the image up beyond its original size. Default: `true`. */
28
+ withoutEnlargement?: boolean;
29
+ /** Output format. Default: keep the input's format. */
30
+ format?: ImageFormat;
31
+ /** Output quality (1-100) for lossy formats (webp/jpeg/avif). */
32
+ quality?: number;
33
+ /**
34
+ * Preserve every frame of multi-frame inputs (animated webp/gif). Default:
35
+ * `true`; harmless for static images. Animation is only retained when the
36
+ * output `format` is animation-capable (`webp`/`gif`).
37
+ */
38
+ animated?: boolean;
39
+ /** Background used when flattening transparency (e.g. for `contain`/jpeg). */
40
+ background?: string;
41
+ }
42
+ /** Result of {@link transformImage}: the processed bytes plus resolved metadata. */
43
+ export interface ImageTransformResult {
44
+ /** The processed image bytes. */
45
+ data: Buffer;
46
+ /** MIME type of the processed image, e.g. `image/webp`. */
47
+ mime: string;
48
+ /** Resolved output format, e.g. `webp`. */
49
+ format: string;
50
+ /** Width of the processed image in px, if sharp could determine it. */
51
+ width?: number;
52
+ /** Height of the processed image in px, if sharp could determine it. */
53
+ height?: number;
54
+ }
10
55
  export interface ExternalRef {
11
56
  /** External system identifier, e.g. 'jira', 'sharepoint' */
12
57
  source: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "description": "File storage module for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,13 +64,15 @@
64
64
  "eslint": "^10.1.0",
65
65
  "ipx": "^3.1.1",
66
66
  "nuxt": "^4.4.2",
67
+ "sharp": "^0.34.5",
67
68
  "typescript": "~6.0.2",
68
69
  "vitest": "^4.1.2",
69
70
  "vue-tsc": "^3.2.6"
70
71
  },
71
72
  "peerDependencies": {
72
73
  "@nuxt/image": "^2.0.0",
73
- "ipx": "^3.0.0"
74
+ "ipx": "^3.0.0",
75
+ "sharp": "^0.33.0 || ^0.34.0"
74
76
  },
75
77
  "peerDependenciesMeta": {
76
78
  "@nuxt/image": {
@@ -78,12 +80,16 @@
78
80
  },
79
81
  "ipx": {
80
82
  "optional": true
83
+ },
84
+ "sharp": {
85
+ "optional": true
81
86
  }
82
87
  },
83
88
  "packageManager": "pnpm@10.33.0",
84
89
  "pnpm": {
85
90
  "onlyBuiltDependencies": [
86
- "esbuild"
91
+ "esbuild",
92
+ "sharp"
87
93
  ]
88
94
  }
89
95
  }
@@ -1,4 +1,4 @@
1
- import { defineEventHandler } from 'h3';
1
+ import { defineEventHandler, useBase, type EventHandler } from 'h3';
2
2
  import { createIPX, createIPXH3Handler, type IPX, type IPXStorage } from 'ipx';
3
3
  // @ts-expect-error virtual module injected by the module
4
4
  import { ipxRoute } from '#nuxt-filer-image';
@@ -41,24 +41,20 @@ const filerStorage: IPXStorage = {
41
41
  },
42
42
  };
43
43
 
44
- let _handler: ReturnType<typeof createIPXH3Handler> | null = null;
44
+ let _handler: EventHandler | null = null;
45
45
  let _ipx: IPX | null = null;
46
- function getHandler() {
46
+ function getHandler(): EventHandler {
47
47
  if (!_handler) {
48
48
  _ipx = createIPX({ storage: filerStorage });
49
- _handler = createIPXH3Handler(_ipx);
49
+ // IPX expects to see `/<modifiers>/<groupId>/<fileId>`, so strip the
50
+ // configured base prefix first. We delegate to h3's `useBase` rather than
51
+ // assigning `event.path` — `event.path` is a getter-only accessor (it
52
+ // reads back `event._path || req.url`), so writing to it throws
53
+ // "Cannot set property path ... which has only a getter". `useBase`
54
+ // rewrites `_path`/`req.url` for the inner handler and restores them after.
55
+ _handler = useBase(ipxRoute, createIPXH3Handler(_ipx));
50
56
  }
51
57
  return _handler;
52
58
  }
53
59
 
54
- export default defineEventHandler((event) => {
55
- // Strip the configured base prefix so IPX sees the URL it expects:
56
- // `/<modifiers>/<groupId>/<fileId>`.
57
- const original = event.path;
58
- let sub = original.startsWith(ipxRoute)
59
- ? original.slice((ipxRoute as string).length)
60
- : original;
61
- if (!sub.startsWith('/')) sub = `/${sub}`;
62
- event.path = sub;
63
- return getHandler()(event);
64
- });
60
+ export default defineEventHandler((event) => getHandler()(event));
@@ -0,0 +1,82 @@
1
+ import type {
2
+ ImageFormat,
3
+ ImageTransformOptions,
4
+ ImageTransformResult,
5
+ } from '../../../runtime/types';
6
+
7
+ export type { ImageFormat, ImageTransformOptions, ImageTransformResult };
8
+
9
+ const MIME_BY_FORMAT: Record<string, string> = {
10
+ webp: 'image/webp',
11
+ png: 'image/png',
12
+ jpeg: 'image/jpeg',
13
+ jpg: 'image/jpeg',
14
+ avif: 'image/avif',
15
+ gif: 'image/gif',
16
+ };
17
+
18
+ // `sharp` is an optional peer dependency — resolve it lazily and cache the
19
+ // module so unrelated installs never pay for the native binary.
20
+ type SharpModule = typeof import('sharp');
21
+ let sharpModule: SharpModule | undefined;
22
+
23
+ async function loadSharp(): Promise<SharpModule> {
24
+ if (sharpModule) return sharpModule;
25
+ try {
26
+ sharpModule = (await import('sharp')).default as unknown as SharpModule;
27
+ }
28
+ catch {
29
+ throw new Error(
30
+ '[nuxt-filer] Image transforms require the optional peer dependency `sharp`. Install it with `npm i sharp`.'
31
+ );
32
+ }
33
+ return sharpModule;
34
+ }
35
+
36
+ /**
37
+ * Process an image buffer with sharp: resize within a box, convert format,
38
+ * optimize, and (by default) preserve animation for multi-frame inputs. Used
39
+ * for upload-time normalization — see `useFileStorage().upload({ transform })`.
40
+ *
41
+ * Requires the optional `sharp` peer dependency; throws a clear error if it is
42
+ * not installed.
43
+ */
44
+ export async function transformImage(
45
+ data: Buffer | Uint8Array,
46
+ options: ImageTransformOptions = {}
47
+ ): Promise<ImageTransformResult> {
48
+ const sharp = await loadSharp();
49
+
50
+ const animated = options.animated ?? true;
51
+ let pipeline = sharp(data, { animated });
52
+
53
+ if (options.width != null || options.height != null) {
54
+ pipeline = pipeline.resize({
55
+ width: options.width,
56
+ height: options.height,
57
+ fit: options.fit ?? 'inside',
58
+ withoutEnlargement: options.withoutEnlargement ?? true,
59
+ background: options.background,
60
+ });
61
+ }
62
+
63
+ if (options.format) {
64
+ const formatOptions
65
+ = options.quality != null ? { quality: options.quality } : {};
66
+ pipeline = pipeline.toFormat(
67
+ options.format as keyof import('sharp').FormatEnum,
68
+ formatOptions
69
+ );
70
+ }
71
+
72
+ const out = await pipeline.toBuffer({ resolveWithObject: true });
73
+ const format = out.info.format;
74
+
75
+ return {
76
+ data: out.data,
77
+ format,
78
+ mime: MIME_BY_FORMAT[format] ?? `image/${format}`,
79
+ width: out.info.width,
80
+ height: out.info.height,
81
+ };
82
+ }
@@ -4,10 +4,20 @@ import type {
4
4
  StoredFile,
5
5
  ExternalRef,
6
6
  FileStorageProvider,
7
+ ImageTransformOptions,
8
+ ImageTransformResult,
7
9
  } from '../../../runtime/types';
8
10
  import { useFileStorageProvider } from '../provider';
11
+ import { transformImage } from './image';
9
12
 
10
- export type { FileMeta, StoredFile, ExternalRef, FileStorageProvider };
13
+ export type {
14
+ FileMeta,
15
+ StoredFile,
16
+ ExternalRef,
17
+ FileStorageProvider,
18
+ ImageTransformOptions,
19
+ ImageTransformResult,
20
+ };
11
21
 
12
22
  export const useFileStorage = () => {
13
23
  const provider = useFileStorageProvider();
@@ -15,9 +25,23 @@ export const useFileStorage = () => {
15
25
  async function upload(
16
26
  groupId: string,
17
27
  data: Buffer | Uint8Array,
18
- options: { meta?: FileMeta } = {}
28
+ options: { meta?: FileMeta; transform?: ImageTransformOptions } = {}
19
29
  ): Promise<string> {
20
- const { id } = await provider.create(groupId, data, options.meta);
30
+ let payload = data;
31
+
32
+ // Optional upload-time image processing (requires the `sharp` peer dep).
33
+ // The stored bytes and the metadata's mime/dimensions reflect the result.
34
+ if (options.transform) {
35
+ const result = await transformImage(data, options.transform);
36
+ payload = result.data;
37
+ if (options.meta) {
38
+ options.meta.mime = result.mime;
39
+ options.meta.width = result.width;
40
+ options.meta.height = result.height;
41
+ }
42
+ }
43
+
44
+ const { id } = await provider.create(groupId, payload, options.meta);
21
45
  return id;
22
46
  }
23
47
 
@@ -8,6 +8,54 @@ export interface FileMeta {
8
8
  [key: string]: unknown;
9
9
  }
10
10
 
11
+ /** Output image format for {@link transformImage}. */
12
+ export type ImageFormat = 'webp' | 'png' | 'jpeg' | 'avif' | 'gif';
13
+
14
+ /**
15
+ * Options for upload-time image processing, backed by the optional `sharp`
16
+ * peer dependency. Passed via `useFileStorage().upload(.., { transform })` or
17
+ * to the standalone `transformImage()` util.
18
+ */
19
+ export interface ImageTransformOptions {
20
+ /** Target width in px. Combined with `fit` to bound the image. */
21
+ width?: number;
22
+ /** Target height in px. Combined with `fit` to bound the image. */
23
+ height?: number;
24
+ /**
25
+ * How the image is resized to fit `width`/`height`. Mirrors sharp's `fit`.
26
+ * Default: `'inside'` (preserve aspect ratio, fit within the box).
27
+ */
28
+ fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
29
+ /** Never scale the image up beyond its original size. Default: `true`. */
30
+ withoutEnlargement?: boolean;
31
+ /** Output format. Default: keep the input's format. */
32
+ format?: ImageFormat;
33
+ /** Output quality (1-100) for lossy formats (webp/jpeg/avif). */
34
+ quality?: number;
35
+ /**
36
+ * Preserve every frame of multi-frame inputs (animated webp/gif). Default:
37
+ * `true`; harmless for static images. Animation is only retained when the
38
+ * output `format` is animation-capable (`webp`/`gif`).
39
+ */
40
+ animated?: boolean;
41
+ /** Background used when flattening transparency (e.g. for `contain`/jpeg). */
42
+ background?: string;
43
+ }
44
+
45
+ /** Result of {@link transformImage}: the processed bytes plus resolved metadata. */
46
+ export interface ImageTransformResult {
47
+ /** The processed image bytes. */
48
+ data: Buffer;
49
+ /** MIME type of the processed image, e.g. `image/webp`. */
50
+ mime: string;
51
+ /** Resolved output format, e.g. `webp`. */
52
+ format: string;
53
+ /** Width of the processed image in px, if sharp could determine it. */
54
+ width?: number;
55
+ /** Height of the processed image in px, if sharp could determine it. */
56
+ height?: number;
57
+ }
58
+
11
59
  export interface ExternalRef {
12
60
  /** External system identifier, e.g. 'jira', 'sharepoint' */
13
61
  source: string;