nuxt-filer 0.0.3 → 0.0.5

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
@@ -15,6 +15,7 @@ File storage module for Nuxt. Provides a server-side `useFileStorage()` composab
15
15
  - **Zero-config default** — works out of the box with local filesystem storage, no database required
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
+ - **`@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
18
19
 
19
20
  ## Quick Setup
20
21
 
@@ -96,6 +97,39 @@ export default defineEventHandler(async (event) => {
96
97
  | `external?.push(groupId, id, data, meta)` | Push to external system |
97
98
  | `external?.pull(groupId, ref)` | Pull from external system |
98
99
 
100
+ ## `@nuxt/image` Integration
101
+
102
+ If `@nuxt/image` is installed alongside `nuxt-filer`, the module automatically registers a `filer` image provider and an IPX endpoint that pulls bytes from your storage provider, runs them through Sharp, and returns the result.
103
+
104
+ ```vue
105
+ <template>
106
+ <NuxtImg
107
+ provider="filer"
108
+ :src="`${groupId}/${fileId}`"
109
+ width="200"
110
+ height="200"
111
+ fit="cover"
112
+ format="webp"
113
+ />
114
+ </template>
115
+ ```
116
+
117
+ Generated URLs look like `/_filer-ipx/w_200,h_200,fit_cover,format_webp/<groupId>/<fileId>` and are served with `cache-control: max-age=...`, `last-modified`, and `etag` for `if-modified-since` / `if-none-match` revalidation.
118
+
119
+ The integration can be configured or turned off:
120
+
121
+ ```ts
122
+ filer: {
123
+ image: {
124
+ enabled: true, // false to disable; 'force' to register without @nuxt/image
125
+ route: '/_filer-ipx', // base path for the IPX endpoint
126
+ providerName: 'filer', // name used in <NuxtImg provider="..." />
127
+ },
128
+ },
129
+ ```
130
+
131
+ `@nuxt/image` and `ipx` are declared as optional peer dependencies — they only need to be installed if you want to use this integration.
132
+
99
133
  ## Custom Provider
100
134
 
101
135
  For advanced use cases (database-backed metadata, S3 storage, external file sync), implement the `FileStorageProvider` interface and register it in a Nitro plugin:
package/dist/module.d.mts CHANGED
@@ -1,5 +1,18 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
+ interface FilerImageOptions {
4
+ /**
5
+ * Enable the @nuxt/image provider + IPX route. When `true` (the default),
6
+ * the integration registers only if `@nuxt/image` is also installed; when
7
+ * `false`, it is never registered. Set to `'force'` to register the routes
8
+ * even when `@nuxt/image` cannot be detected (mainly useful for tests).
9
+ */
10
+ enabled?: boolean | 'force';
11
+ /** Base path for the IPX endpoint. Default: `/_filer-ipx`. */
12
+ route?: string;
13
+ /** Name to register the @nuxt/image provider under. Default: `filer`. */
14
+ providerName?: string;
15
+ }
3
16
  interface ModuleOptions {
4
17
  /** Nitro storage mount name for binary file data. Default: 'documents' */
5
18
  storageName?: string;
@@ -7,8 +20,10 @@ interface ModuleOptions {
7
20
  storagePath?: string;
8
21
  /** Provider mode. 'unstorage' uses the built-in unstorage provider. 'custom' expects you to call setFileStorageProvider() in a Nitro plugin. Default: 'unstorage' */
9
22
  provider?: 'unstorage' | 'custom';
23
+ /** @nuxt/image integration. Set to `false` to disable, or pass an object to override defaults. */
24
+ image?: boolean | FilerImageOptions;
10
25
  }
11
26
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
12
27
 
13
28
  export { _default as default };
14
- export type { ModuleOptions };
29
+ export type { FilerImageOptions, ModuleOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
3
  "configKey": "filer",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addServerImports, addImports } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addServerImports, addImports, hasNuxtModule, addServerHandler } from '@nuxt/kit';
2
2
  import { consola } from 'consola';
3
3
 
4
4
  const module = defineNuxtModule({
@@ -9,7 +9,8 @@ const module = defineNuxtModule({
9
9
  defaults: {
10
10
  storageName: "documents",
11
11
  storagePath: ".data/documents",
12
- provider: "unstorage"
12
+ provider: "unstorage",
13
+ image: true
13
14
  },
14
15
  setup(options, nuxt) {
15
16
  const resolver = createResolver(import.meta.url);
@@ -42,9 +43,34 @@ const module = defineNuxtModule({
42
43
  type: true
43
44
  }
44
45
  ]);
46
+ const imageOpt = typeof options.image === "object" && options.image !== null ? options.image : {};
47
+ const imageEnabled = options.image !== false && (imageOpt.enabled ?? true) !== false;
48
+ const ipxRoute = (imageOpt.route ?? "/_filer-ipx").replace(/\/+$/, "");
49
+ const providerName = imageOpt.providerName ?? "filer";
50
+ const shouldRegisterImage = imageEnabled && (imageOpt.enabled === "force" || hasNuxtModule("@nuxt/image"));
51
+ if (shouldRegisterImage) {
52
+ addServerHandler({
53
+ route: `${ipxRoute}/**`,
54
+ handler: resolver.resolve("./runtime/server/handlers/ipx")
55
+ });
56
+ const imageConfig = nuxt.options.image ?? {};
57
+ const providers = imageConfig.providers ?? {};
58
+ providers[providerName] = {
59
+ name: providerName,
60
+ provider: resolver.resolve("./runtime/image/provider"),
61
+ options: { baseURL: ipxRoute }
62
+ };
63
+ imageConfig.providers = providers;
64
+ nuxt.options.image = imageConfig;
65
+ } else if (imageEnabled && options.image !== false) {
66
+ consola.info(
67
+ "nuxt-filer: @nuxt/image not detected \u2014 IPX integration disabled. Install `@nuxt/image` to enable optimized image variants."
68
+ );
69
+ }
45
70
  nuxt.hook("nitro:config", (nitroConfig) => {
46
71
  nitroConfig.virtual = nitroConfig.virtual || {};
47
72
  nitroConfig.virtual["#nuxt-filer-options"] = `export const storageName = ${JSON.stringify(options.storageName)}`;
73
+ nitroConfig.virtual["#nuxt-filer-image"] = `export const ipxRoute = ${JSON.stringify(ipxRoute)}`;
48
74
  if (options.provider === "unstorage") {
49
75
  nitroConfig.storage = nitroConfig.storage || {};
50
76
  nitroConfig.storage[options.storageName] = {
@@ -0,0 +1,7 @@
1
+ import type { ProviderGetImage } from '@nuxt/image';
2
+ interface FilerProviderOptions {
3
+ baseURL?: string;
4
+ }
5
+ export declare const getImage: ProviderGetImage;
6
+ export declare const validateDomains = false;
7
+ export type { FilerProviderOptions };
@@ -0,0 +1,17 @@
1
+ const operationsGenerator = (modifiers) => {
2
+ const ops = [];
3
+ for (const [key, value] of Object.entries(modifiers)) {
4
+ if (value === void 0 || value === null || value === "" || value === false) continue;
5
+ ops.push(`${key}_${value}`);
6
+ }
7
+ return ops.length ? ops.join(",") : "_";
8
+ };
9
+ export const getImage = (src, { modifiers = {}, baseURL } = {}) => {
10
+ const root = (baseURL ?? "/_filer-ipx").replace(/\/+$/, "");
11
+ const ops = operationsGenerator(
12
+ modifiers
13
+ );
14
+ const path = src.replace(/^\/+/, "");
15
+ return { url: `${root}/${ops}/${path}` };
16
+ };
17
+ export const validateDomains = false;
@@ -0,0 +1,6 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | void | Buffer<ArrayBufferLike> | {
2
+ error: {
3
+ message: string;
4
+ };
5
+ }>>;
6
+ export default _default;
@@ -0,0 +1,49 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { createIPX, createIPXH3Handler } from "ipx";
3
+ import { ipxRoute } from "#nuxt-filer-image";
4
+ import { useFileStorageProvider } from "../provider.js";
5
+ function parseId(id) {
6
+ const lastSlash = id.lastIndexOf("/");
7
+ if (lastSlash <= 0 || lastSlash === id.length - 1) return null;
8
+ return [id.slice(0, lastSlash), id.slice(lastSlash + 1)];
9
+ }
10
+ const filerStorage = {
11
+ name: "nuxt-filer",
12
+ async getMeta(id) {
13
+ const parsed = parseId(id);
14
+ if (!parsed) return void 0;
15
+ const [groupId, fileId] = parsed;
16
+ const provider = useFileStorageProvider();
17
+ const file = await provider.get(groupId, fileId);
18
+ if (!file) return void 0;
19
+ const mtime = file.updatedAt ?? file.createdAt ?? /* @__PURE__ */ new Date();
20
+ return {
21
+ mtime,
22
+ maxAge: 60 * 60 * 24 * 365
23
+ };
24
+ },
25
+ async getData(id) {
26
+ const parsed = parseId(id);
27
+ if (!parsed) return void 0;
28
+ const [groupId, fileId] = parsed;
29
+ const data = await useFileStorageProvider().getData(groupId, fileId);
30
+ if (!data) return void 0;
31
+ return data;
32
+ }
33
+ };
34
+ let _handler = null;
35
+ let _ipx = null;
36
+ function getHandler() {
37
+ if (!_handler) {
38
+ _ipx = createIPX({ storage: filerStorage });
39
+ _handler = createIPXH3Handler(_ipx);
40
+ }
41
+ return _handler;
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
+ });
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { useStorage } from "nitropack/runtime";
2
3
  export function createUnstorageProvider(storageName) {
3
4
  function getStorage() {
4
5
  return useStorage(storageName);
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
- export { type ModuleOptions } from './module.mjs'
3
+ export { type FilerImageOptions, type ModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "File storage module for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,17 +55,31 @@
55
55
  "devDependencies": {
56
56
  "@nuxt/devtools": "^3.2.4",
57
57
  "@nuxt/eslint-config": "^1.15.2",
58
+ "@nuxt/image": "^2.0.0",
58
59
  "@nuxt/module-builder": "^1.0.2",
59
60
  "@nuxt/schema": "^4.4.2",
60
61
  "@nuxt/test-utils": "^4.0.0",
61
62
  "@types/node": "latest",
62
63
  "changelogen": "^0.6.2",
63
64
  "eslint": "^10.1.0",
65
+ "ipx": "^3.1.1",
64
66
  "nuxt": "^4.4.2",
65
67
  "typescript": "~6.0.2",
66
68
  "vitest": "^4.1.2",
67
69
  "vue-tsc": "^3.2.6"
68
70
  },
71
+ "peerDependencies": {
72
+ "@nuxt/image": "^2.0.0",
73
+ "ipx": "^3.0.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@nuxt/image": {
77
+ "optional": true
78
+ },
79
+ "ipx": {
80
+ "optional": true
81
+ }
82
+ },
69
83
  "packageManager": "pnpm@10.33.0",
70
84
  "pnpm": {
71
85
  "onlyBuiltDependencies": [
@@ -0,0 +1,33 @@
1
+ import type { ProviderGetImage } from '@nuxt/image';
2
+
3
+ interface FilerProviderOptions {
4
+ baseURL?: string;
5
+ }
6
+
7
+ const operationsGenerator = (
8
+ modifiers: Record<string, string | number | boolean | undefined>
9
+ ): string => {
10
+ const ops: string[] = [];
11
+ for (const [key, value] of Object.entries(modifiers)) {
12
+ if (value === undefined || value === null || value === '' || value === false) continue;
13
+ ops.push(`${key}_${value}`);
14
+ }
15
+ return ops.length ? ops.join(',') : '_';
16
+ };
17
+
18
+ export const getImage: ProviderGetImage = (
19
+ src: string,
20
+ { modifiers = {}, baseURL }: { modifiers?: Record<string, unknown>; baseURL?: string } = {},
21
+ // ctx is provided by @nuxt/image but unused here
22
+ ) => {
23
+ const root = (baseURL ?? '/_filer-ipx').replace(/\/+$/, '');
24
+ const ops = operationsGenerator(
25
+ modifiers as Record<string, string | number | boolean | undefined>
26
+ );
27
+ const path = src.replace(/^\/+/, '');
28
+ return { url: `${root}/${ops}/${path}` };
29
+ };
30
+
31
+ export const validateDomains = false;
32
+
33
+ export type { FilerProviderOptions };
@@ -0,0 +1,64 @@
1
+ import { defineEventHandler } from 'h3';
2
+ import { createIPX, createIPXH3Handler, type IPX, type IPXStorage } from 'ipx';
3
+ // @ts-expect-error virtual module injected by the module
4
+ import { ipxRoute } from '#nuxt-filer-image';
5
+ import { useFileStorageProvider } from '../provider';
6
+
7
+ /**
8
+ * Maps an IPX `id` (the path after the modifiers segment) to a `(groupId, fileId)`
9
+ * pair as used by the file storage provider. Ids are expected as
10
+ * `groupId/fileId` — additional `/` characters in the group id are preserved.
11
+ */
12
+ function parseId(id: string): [string, string] | null {
13
+ const lastSlash = id.lastIndexOf('/');
14
+ if (lastSlash <= 0 || lastSlash === id.length - 1) return null;
15
+ return [id.slice(0, lastSlash), id.slice(lastSlash + 1)];
16
+ }
17
+
18
+ const filerStorage: IPXStorage = {
19
+ name: 'nuxt-filer',
20
+ async getMeta(id) {
21
+ const parsed = parseId(id);
22
+ if (!parsed) return undefined;
23
+ const [groupId, fileId] = parsed;
24
+ const provider = useFileStorageProvider();
25
+ const file = await provider.get(groupId, fileId);
26
+ if (!file) return undefined;
27
+ const mtime = file.updatedAt ?? file.createdAt ?? new Date();
28
+ return {
29
+ mtime,
30
+ maxAge: 60 * 60 * 24 * 365,
31
+ };
32
+ },
33
+ async getData(id) {
34
+ const parsed = parseId(id);
35
+ if (!parsed) return undefined;
36
+ const [groupId, fileId] = parsed;
37
+ const data = await useFileStorageProvider().getData(groupId, fileId);
38
+ if (!data) return undefined;
39
+ // IPX accepts ArrayBuffer | Buffer; pass the buffer view directly.
40
+ return data as unknown as ArrayBuffer;
41
+ },
42
+ };
43
+
44
+ let _handler: ReturnType<typeof createIPXH3Handler> | null = null;
45
+ let _ipx: IPX | null = null;
46
+ function getHandler() {
47
+ if (!_handler) {
48
+ _ipx = createIPX({ storage: filerStorage });
49
+ _handler = createIPXH3Handler(_ipx);
50
+ }
51
+ return _handler;
52
+ }
53
+
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
+ });
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { useStorage } from 'nitropack/runtime';
2
3
  import type {
3
4
  FileStorageProvider,
4
5
  FileMeta,