nuxt-filer 0.0.4 → 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 +34 -0
- package/dist/module.d.mts +16 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -2
- package/dist/runtime/image/provider.d.ts +7 -0
- package/dist/runtime/image/provider.js +17 -0
- package/dist/runtime/server/handlers/ipx.d.ts +6 -0
- package/dist/runtime/server/handlers/ipx.js +49 -0
- package/dist/types.d.mts +1 -1
- package/package.json +15 -1
- package/src/runtime/image/provider.ts +33 -0
- package/src/runtime/server/handlers/ipx.ts +64 -0
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
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,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,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
|
+
});
|
package/dist/types.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-filer",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
});
|