nuxt-filer 0.0.6 → 0.0.8

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-filer",
3
3
  "configKey": "filer",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -73,15 +73,16 @@ const module = defineNuxtModule({
73
73
  }
74
74
  nuxt.hook("nitro:config", (nitroConfig) => {
75
75
  nitroConfig.virtual = nitroConfig.virtual || {};
76
- nitroConfig.virtual["#nuxt-filer-options"] = `export const storageName = ${JSON.stringify(options.storageName)}`;
76
+ nitroConfig.virtual["#nuxt-filer-options"] = [
77
+ `export const storageName = ${JSON.stringify(options.storageName)};`,
78
+ `export const storagePath = ${JSON.stringify(options.storagePath)};`
79
+ ].join("\n");
77
80
  nitroConfig.virtual["#nuxt-filer-image"] = `export const ipxRoute = ${JSON.stringify(ipxRoute)}`;
78
81
  if (options.provider === "unstorage") {
79
- nitroConfig.storage = nitroConfig.storage || {};
80
- nitroConfig.storage[options.storageName] = {
81
- driver: "fsLite",
82
- base: options.storagePath
83
- };
84
82
  nitroConfig.plugins = nitroConfig.plugins || [];
83
+ nitroConfig.plugins.push(
84
+ resolver.resolve("./runtime/server/plugins/default-storage")
85
+ );
85
86
  nitroConfig.plugins.push(
86
87
  resolver.resolve("./runtime/server/plugins/default-provider")
87
88
  );
@@ -3,5 +3,10 @@ interface FilerProviderOptions {
3
3
  baseURL?: string;
4
4
  }
5
5
  export declare const getImage: ProviderGetImage;
6
- export declare const validateDomains = false;
6
+ declare const _default: () => {
7
+ getImage: ProviderGetImage;
8
+ validateDomains: boolean;
9
+ supportsAlias: boolean;
10
+ };
11
+ export default _default;
7
12
  export type { FilerProviderOptions };
@@ -1,17 +1,35 @@
1
+ import { joinURL } from "ufo";
1
2
  const operationsGenerator = (modifiers) => {
2
- const ops = [];
3
- for (const [key, value] of Object.entries(modifiers)) {
3
+ const out = [];
4
+ if (modifiers.width && modifiers.height) {
5
+ modifiers.resize = `${modifiers.width}x${modifiers.height}`;
6
+ delete modifiers.width;
7
+ delete modifiers.height;
8
+ }
9
+ const keyMap = {
10
+ format: "f",
11
+ width: "w",
12
+ height: "h",
13
+ resize: "s",
14
+ quality: "q",
15
+ background: "b",
16
+ position: "pos"
17
+ };
18
+ for (const [rawKey, value] of Object.entries(modifiers)) {
4
19
  if (value === void 0 || value === null || value === "" || value === false) continue;
5
- ops.push(`${key}_${value}`);
20
+ const key = keyMap[rawKey] ?? rawKey;
21
+ out.push(`${key}_${value}`);
6
22
  }
7
- return ops.length ? ops.join(",") : "_";
23
+ return out.length ? out.join(",") : "_";
8
24
  };
9
- export const getImage = (src, { modifiers = {}, baseURL } = {}) => {
10
- const root = (baseURL ?? "/_filer-ipx").replace(/\/+$/, "");
25
+ export const getImage = (src, { modifiers = {}, baseURL = "/_filer-ipx" } = {}) => {
11
26
  const ops = operationsGenerator(
12
27
  modifiers
13
28
  );
14
- const path = src.replace(/^\/+/, "");
15
- return { url: `${root}/${ops}/${path}` };
29
+ return { url: joinURL(baseURL, ops, src.replace(/^\/+/, "")) };
16
30
  };
17
- export const validateDomains = false;
31
+ export default () => ({
32
+ getImage,
33
+ validateDomains: false,
34
+ supportsAlias: false
35
+ });
@@ -0,0 +1,34 @@
1
+ export interface FsDriverOptions {
2
+ /** Filesystem path used as the base for all keys. Required. */
3
+ base?: string;
4
+ /** Treat the storage as read-only; writes become no-ops. */
5
+ readOnly?: boolean;
6
+ /** Suppress `clear()`. */
7
+ noClear?: boolean;
8
+ /** Optional ignore filter applied during `getKeys`. */
9
+ ignore?: (path: string) => boolean;
10
+ }
11
+ export default function fsDriver(opts?: FsDriverOptions): {
12
+ name: string;
13
+ options: FsDriverOptions;
14
+ flags: {
15
+ maxDepth: boolean;
16
+ };
17
+ hasItem(key: string): boolean;
18
+ getItem(key: string): Promise<string | null>;
19
+ getItemRaw(key: string): Promise<Buffer<ArrayBufferLike> | null>;
20
+ getMeta(key: string): Promise<{
21
+ atime: Date | undefined;
22
+ mtime: Date | undefined;
23
+ size: number | undefined;
24
+ birthtime: Date | undefined;
25
+ ctime: Date | undefined;
26
+ }>;
27
+ setItem(key: string, value: string): Promise<void>;
28
+ setItemRaw(key: string, value: Buffer | Uint8Array): Promise<void>;
29
+ removeItem(key: string): Promise<void>;
30
+ getKeys(_base?: string, topts?: {
31
+ maxDepth?: number;
32
+ }): Promise<string[]>;
33
+ clear(): Promise<void>;
34
+ };
@@ -0,0 +1,106 @@
1
+ import {
2
+ existsSync,
3
+ promises as fsp
4
+ } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ const PATH_TRAVERSE_RE = /\.\.:|\.\.$/;
7
+ const DRIVER_NAME = "nuxt-filer-fs";
8
+ function driverError(message) {
9
+ return new Error(`[nuxt-filer] [${DRIVER_NAME}] ${message}`);
10
+ }
11
+ function ignoreNotfound(err) {
12
+ if (err.code === "ENOENT" || err.code === "EISDIR") return null;
13
+ throw err;
14
+ }
15
+ async function readdirSafe(dir) {
16
+ try {
17
+ return await fsp.readdir(dir, { withFileTypes: true });
18
+ } catch (err) {
19
+ if (err.code === "ENOENT") return [];
20
+ throw err;
21
+ }
22
+ }
23
+ async function readdirRecursive(dir, ignore, maxDepth) {
24
+ if (ignore && ignore(dir)) return [];
25
+ const entries = await readdirSafe(dir);
26
+ const files = [];
27
+ await Promise.all(
28
+ entries.map(async (entry) => {
29
+ const entryPath = resolve(dir, entry.name);
30
+ if (entry.isDirectory()) {
31
+ if (maxDepth === void 0 || maxDepth > 0) {
32
+ const nested = await readdirRecursive(
33
+ entryPath,
34
+ ignore,
35
+ maxDepth === void 0 ? void 0 : maxDepth - 1
36
+ );
37
+ files.push(...nested.map((f) => entry.name + "/" + f));
38
+ }
39
+ } else if (!(ignore && ignore(entry.name))) {
40
+ files.push(entry.name);
41
+ }
42
+ })
43
+ );
44
+ return files;
45
+ }
46
+ export default function fsDriver(opts = {}) {
47
+ if (!opts.base) {
48
+ throw driverError("Missing required option `base`.");
49
+ }
50
+ const base = resolve(opts.base);
51
+ const r = (key) => {
52
+ if (PATH_TRAVERSE_RE.test(key)) {
53
+ throw driverError(
54
+ `Invalid key: ${JSON.stringify(key)}. It should not contain .. segments`
55
+ );
56
+ }
57
+ return join(base, key.replace(/:/g, "/"));
58
+ };
59
+ async function write(path, value) {
60
+ await fsp.mkdir(dirname(path), { recursive: true });
61
+ await fsp.writeFile(path, value);
62
+ }
63
+ return {
64
+ name: DRIVER_NAME,
65
+ options: opts,
66
+ flags: { maxDepth: true },
67
+ hasItem(key) {
68
+ return existsSync(r(key));
69
+ },
70
+ getItem(key) {
71
+ return fsp.readFile(r(key), "utf8").catch((err) => ignoreNotfound(err));
72
+ },
73
+ getItemRaw(key) {
74
+ return fsp.readFile(r(key)).catch((err) => ignoreNotfound(err));
75
+ },
76
+ async getMeta(key) {
77
+ const stat = await fsp.stat(r(key)).catch(() => ({}));
78
+ return {
79
+ atime: stat.atime,
80
+ mtime: stat.mtime,
81
+ size: stat.size,
82
+ birthtime: stat.birthtime,
83
+ ctime: stat.ctime
84
+ };
85
+ },
86
+ async setItem(key, value) {
87
+ if (opts.readOnly) return;
88
+ await write(r(key), value);
89
+ },
90
+ async setItemRaw(key, value) {
91
+ if (opts.readOnly) return;
92
+ await write(r(key), value);
93
+ },
94
+ async removeItem(key) {
95
+ if (opts.readOnly) return;
96
+ await fsp.unlink(r(key)).catch((err) => ignoreNotfound(err));
97
+ },
98
+ getKeys(_base, topts) {
99
+ return readdirRecursive(r("."), opts.ignore, topts?.maxDepth);
100
+ },
101
+ async clear() {
102
+ if (opts.readOnly || opts.noClear) return;
103
+ await fsp.rm(r("."), { recursive: true, force: true });
104
+ }
105
+ };
106
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mount the default filesystem-backed storage for nuxt-filer. We mount
3
+ * via a Nitro plugin rather than `nitroConfig.storage` so that our
4
+ * custom fs driver is bundled with the plugin and there is no runtime
5
+ * module resolution against the package's `dist/`.
6
+ */
7
+ declare const _default: import("nitropack/types").NitroAppPlugin;
8
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { defineNitroPlugin, useStorage } from "nitropack/runtime";
2
+ import { storageName, storagePath } from "#nuxt-filer-options";
3
+ import fsDriver from "../drivers/fs.js";
4
+ export default defineNitroPlugin(() => {
5
+ const storage = useStorage();
6
+ storage.mount(storageName, fsDriver({ base: storagePath }));
7
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-filer",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "File storage module for Nuxt",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,5 @@
1
1
  import type { ProviderGetImage } from '@nuxt/image';
2
+ import { joinURL } from 'ufo';
2
3
 
3
4
  interface FilerProviderOptions {
4
5
  baseURL?: string;
@@ -7,27 +8,44 @@ interface FilerProviderOptions {
7
8
  const operationsGenerator = (
8
9
  modifiers: Record<string, string | number | boolean | undefined>
9
10
  ): string => {
10
- const ops: string[] = [];
11
- for (const [key, value] of Object.entries(modifiers)) {
11
+ const out: string[] = [];
12
+ // Match @nuxt/image's built-in IPX provider: width+height collapses to resize.
13
+ if (modifiers.width && modifiers.height) {
14
+ modifiers.resize = `${modifiers.width}x${modifiers.height}`;
15
+ delete modifiers.width;
16
+ delete modifiers.height;
17
+ }
18
+ const keyMap: Record<string, string> = {
19
+ format: 'f',
20
+ width: 'w',
21
+ height: 'h',
22
+ resize: 's',
23
+ quality: 'q',
24
+ background: 'b',
25
+ position: 'pos',
26
+ };
27
+ for (const [rawKey, value] of Object.entries(modifiers)) {
12
28
  if (value === undefined || value === null || value === '' || value === false) continue;
13
- ops.push(`${key}_${value}`);
29
+ const key = keyMap[rawKey] ?? rawKey;
30
+ out.push(`${key}_${value}`);
14
31
  }
15
- return ops.length ? ops.join(',') : '_';
32
+ return out.length ? out.join(',') : '_';
16
33
  };
17
34
 
18
35
  export const getImage: ProviderGetImage = (
19
36
  src: string,
20
- { modifiers = {}, baseURL }: { modifiers?: Record<string, unknown>; baseURL?: string } = {},
21
- // ctx is provided by @nuxt/image but unused here
37
+ { modifiers = {}, baseURL = '/_filer-ipx' }: { modifiers?: Record<string, unknown>; baseURL?: string } = {},
22
38
  ) => {
23
- const root = (baseURL ?? '/_filer-ipx').replace(/\/+$/, '');
24
39
  const ops = operationsGenerator(
25
40
  modifiers as Record<string, string | number | boolean | undefined>
26
41
  );
27
- const path = src.replace(/^\/+/, '');
28
- return { url: `${root}/${ops}/${path}` };
42
+ return { url: joinURL(baseURL, ops, src.replace(/^\/+/, '')) };
29
43
  };
30
44
 
31
- export const validateDomains = false;
45
+ export default () => ({
46
+ getImage,
47
+ validateDomains: false,
48
+ supportsAlias: false,
49
+ });
32
50
 
33
51
  export type { FilerProviderOptions };
@@ -0,0 +1,156 @@
1
+ import {
2
+ existsSync,
3
+ promises as fsp,
4
+ type Dirent,
5
+ type Stats,
6
+ } from 'node:fs';
7
+ import { dirname, join, resolve } from 'node:path';
8
+
9
+ /**
10
+ * Drop-in replacement for unstorage's built-in `fs-lite` driver.
11
+ *
12
+ * `fs-lite` creates intermediate directories via a userspace `ensuredir`
13
+ * (recursive `existsSync` + non-recursive `mkdir`). In practice this is
14
+ * unreliable for first-time writes to a brand-new key path — we observed
15
+ * ENOENT on `writeFile` even though `ensuredir` should have run. The
16
+ * kernel's atomic `mkdir(..., { recursive: true })` is much more reliable
17
+ * across filesystems (including container-bound volumes).
18
+ *
19
+ * This driver mirrors the fs-lite public surface but always pre-creates
20
+ * the parent directory tree with a single recursive `mkdir` before any
21
+ * write, and tolerates missing files / dirs on reads.
22
+ *
23
+ * Implemented against unstorage's structural `Driver` contract; we avoid
24
+ * importing `defineDriver` so this stays decoupled from a specific
25
+ * unstorage major.
26
+ */
27
+
28
+ const PATH_TRAVERSE_RE = /\.\.:|\.\.$/;
29
+ const DRIVER_NAME = 'nuxt-filer-fs';
30
+
31
+ export interface FsDriverOptions {
32
+ /** Filesystem path used as the base for all keys. Required. */
33
+ base?: string;
34
+ /** Treat the storage as read-only; writes become no-ops. */
35
+ readOnly?: boolean;
36
+ /** Suppress `clear()`. */
37
+ noClear?: boolean;
38
+ /** Optional ignore filter applied during `getKeys`. */
39
+ ignore?: (path: string) => boolean;
40
+ }
41
+
42
+ function driverError(message: string): Error {
43
+ return new Error(`[nuxt-filer] [${DRIVER_NAME}] ${message}`);
44
+ }
45
+
46
+ function ignoreNotfound<T>(err: NodeJS.ErrnoException): T | null {
47
+ if (err.code === 'ENOENT' || err.code === 'EISDIR') return null;
48
+ throw err;
49
+ }
50
+
51
+ async function readdirSafe(dir: string): Promise<Dirent[]> {
52
+ try {
53
+ return await fsp.readdir(dir, { withFileTypes: true });
54
+ } catch (err) {
55
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
56
+ throw err;
57
+ }
58
+ }
59
+
60
+ async function readdirRecursive(
61
+ dir: string,
62
+ ignore: ((p: string) => boolean) | undefined,
63
+ maxDepth: number | undefined,
64
+ ): Promise<string[]> {
65
+ if (ignore && ignore(dir)) return [];
66
+ const entries = await readdirSafe(dir);
67
+ const files: string[] = [];
68
+ await Promise.all(
69
+ entries.map(async (entry) => {
70
+ const entryPath = resolve(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ if (maxDepth === undefined || maxDepth > 0) {
73
+ const nested = await readdirRecursive(
74
+ entryPath,
75
+ ignore,
76
+ maxDepth === undefined ? undefined : maxDepth - 1,
77
+ );
78
+ files.push(...nested.map((f) => entry.name + '/' + f));
79
+ }
80
+ } else if (!(ignore && ignore(entry.name))) {
81
+ files.push(entry.name);
82
+ }
83
+ }),
84
+ );
85
+ return files;
86
+ }
87
+
88
+ export default function fsDriver(opts: FsDriverOptions = {}) {
89
+ if (!opts.base) {
90
+ throw driverError('Missing required option `base`.');
91
+ }
92
+ const base = resolve(opts.base);
93
+
94
+ const r = (key: string) => {
95
+ if (PATH_TRAVERSE_RE.test(key)) {
96
+ throw driverError(
97
+ `Invalid key: ${JSON.stringify(key)}. It should not contain .. segments`,
98
+ );
99
+ }
100
+ return join(base, key.replace(/:/g, '/'));
101
+ };
102
+
103
+ async function write(path: string, value: string | Buffer | Uint8Array) {
104
+ await fsp.mkdir(dirname(path), { recursive: true });
105
+ await fsp.writeFile(path, value);
106
+ }
107
+
108
+ return {
109
+ name: DRIVER_NAME,
110
+ options: opts,
111
+ flags: { maxDepth: true },
112
+
113
+ hasItem(key: string) {
114
+ return existsSync(r(key));
115
+ },
116
+ getItem(key: string) {
117
+ return fsp
118
+ .readFile(r(key), 'utf8')
119
+ .catch((err) => ignoreNotfound<string>(err));
120
+ },
121
+ getItemRaw(key: string) {
122
+ return fsp
123
+ .readFile(r(key))
124
+ .catch((err) => ignoreNotfound<Buffer>(err));
125
+ },
126
+ async getMeta(key: string) {
127
+ const stat = (await fsp.stat(r(key)).catch(() => ({}))) as Partial<Stats>;
128
+ return {
129
+ atime: stat.atime,
130
+ mtime: stat.mtime,
131
+ size: stat.size,
132
+ birthtime: stat.birthtime,
133
+ ctime: stat.ctime,
134
+ };
135
+ },
136
+ async setItem(key: string, value: string) {
137
+ if (opts.readOnly) return;
138
+ await write(r(key), value);
139
+ },
140
+ async setItemRaw(key: string, value: Buffer | Uint8Array) {
141
+ if (opts.readOnly) return;
142
+ await write(r(key), value);
143
+ },
144
+ async removeItem(key: string) {
145
+ if (opts.readOnly) return;
146
+ await fsp.unlink(r(key)).catch((err) => ignoreNotfound(err));
147
+ },
148
+ getKeys(_base?: string, topts?: { maxDepth?: number }) {
149
+ return readdirRecursive(r('.'), opts.ignore, topts?.maxDepth);
150
+ },
151
+ async clear() {
152
+ if (opts.readOnly || opts.noClear) return;
153
+ await fsp.rm(r('.'), { recursive: true, force: true });
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,15 @@
1
+ import { defineNitroPlugin, useStorage } from 'nitropack/runtime';
2
+ // @ts-expect-error virtual module
3
+ import { storageName, storagePath } from '#nuxt-filer-options';
4
+ import fsDriver from '../drivers/fs';
5
+
6
+ /**
7
+ * Mount the default filesystem-backed storage for nuxt-filer. We mount
8
+ * via a Nitro plugin rather than `nitroConfig.storage` so that our
9
+ * custom fs driver is bundled with the plugin and there is no runtime
10
+ * module resolution against the package's `dist/`.
11
+ */
12
+ export default defineNitroPlugin(() => {
13
+ const storage = useStorage();
14
+ storage.mount(storageName, fsDriver({ base: storagePath }));
15
+ });