@howells/stow-next 2.1.0 → 2.4.0

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
@@ -37,12 +37,7 @@ export const POST = createUploadHandler({
37
37
  import { UploadDropzone } from "@howells/stow-react";
38
38
 
39
39
  export default function Page() {
40
- return (
41
- <UploadDropzone
42
- endpoint="/api/upload"
43
- onUploadComplete={(files) => console.log(files)}
44
- />
45
- );
40
+ return <UploadDropzone endpoint="/api/upload" onUploadComplete={(files) => console.log(files)} />;
46
41
  }
47
42
  ```
48
43
 
@@ -60,9 +55,9 @@ export const POST = createUploadHandler({
60
55
  stow: new StowServer(process.env.STOW_API_KEY!),
61
56
 
62
57
  // Optional: File restrictions
63
- maxSize: 10 * 1024 * 1024, // Max file size in bytes
64
- allowedTypes: ["image/*", ".pdf"], // Allowed MIME types or extensions
65
- route: "uploads", // Default route/folder
58
+ maxSize: 10 * 1024 * 1024, // Max file size in bytes
59
+ allowedTypes: ["image/*", ".pdf"], // Allowed MIME types or extensions
60
+ route: "uploads", // Default route/folder
66
61
 
67
62
  // Optional: Custom validation
68
63
  validate: async (file) => {
@@ -139,10 +134,7 @@ export default createStowLoader({
139
134
  ## TypeScript
140
135
 
141
136
  ```typescript
142
- import type {
143
- UploadHandlerConfig,
144
- StowLoaderConfig,
145
- } from "@howells/stow-next";
137
+ import type { UploadHandlerConfig, StowLoaderConfig } from "@howells/stow-next";
146
138
  ```
147
139
 
148
140
  ## Complete Example
@@ -0,0 +1,99 @@
1
+ // src/image-loader.ts
2
+ var STOW_DOMAIN_PATTERN = /\.stow\.sh$/;
3
+ function isStowUrl(src) {
4
+ try {
5
+ const url = new URL(src);
6
+ return STOW_DOMAIN_PATTERN.test(url.hostname);
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+ function buildTransformParams(width, quality, config) {
12
+ const params = new URLSearchParams();
13
+ params.set("w", width.toString());
14
+ if (config.aspectRatio) {
15
+ params.set("h", Math.round(width / config.aspectRatio).toString());
16
+ }
17
+ params.set("q", quality.toString());
18
+ if (config.fit) {
19
+ params.set("fit", config.fit);
20
+ }
21
+ if (config.gravity) {
22
+ params.set("gravity", config.gravity);
23
+ }
24
+ if (config.defaultFormat) {
25
+ params.set("f", config.defaultFormat);
26
+ }
27
+ return params;
28
+ }
29
+ function transformStowUrl(src, baseUrl, params) {
30
+ const url = new URL(src, baseUrl);
31
+ const { pathname } = url;
32
+ if (pathname.startsWith("/files/")) {
33
+ const transformPath = pathname.replace("/files/", "/transform/");
34
+ return `${baseUrl}${transformPath}?${params.toString()}`;
35
+ }
36
+ if (pathname.startsWith("/transform/")) {
37
+ return `${baseUrl}${pathname}?${params.toString()}`;
38
+ }
39
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
40
+ }
41
+ function createStowLoader(config = {}) {
42
+ const {
43
+ baseUrl = "https://stow.sh",
44
+ customDomains,
45
+ defaultQuality = 75,
46
+ defaultFormat,
47
+ proxySlug,
48
+ fit,
49
+ gravity,
50
+ aspectRatio
51
+ } = config;
52
+ const customDomainSet = new Set(customDomains);
53
+ return function stowLoader2({ src, width, quality }) {
54
+ const resolvedQuality = quality || defaultQuality;
55
+ const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
56
+ const isAbsoluteUrl = src.startsWith("http://") || src.startsWith("https://");
57
+ if (customDomainSet.size > 0 && isAbsoluteUrl) {
58
+ const url = new URL(src);
59
+ if (customDomainSet.has(url.hostname)) {
60
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
61
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
62
+ }
63
+ }
64
+ if (isStowUrl(src)) {
65
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
66
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
67
+ }
68
+ if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
69
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
70
+ return transformStowUrl(src, baseUrl, params);
71
+ }
72
+ if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
73
+ const params = new URLSearchParams();
74
+ params.set("url", src);
75
+ params.set("w", width.toString());
76
+ if (aspectRatio) {
77
+ params.set("h", Math.round(width / aspectRatio).toString());
78
+ }
79
+ params.set("q", resolvedQuality.toString());
80
+ if (fit) {
81
+ params.set("fit", fit);
82
+ }
83
+ if (gravity) {
84
+ params.set("gravity", gravity);
85
+ }
86
+ if (defaultFormat) {
87
+ params.set("f", defaultFormat);
88
+ }
89
+ return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
90
+ }
91
+ return src;
92
+ };
93
+ }
94
+ var stowLoader = createStowLoader();
95
+
96
+ export {
97
+ createStowLoader,
98
+ stowLoader
99
+ };
@@ -38,13 +38,22 @@
38
38
  * }
39
39
  * ```
40
40
  */
41
- /** Next.js image loader input contract. */
41
+ /**
42
+ * Next.js image loader input contract.
43
+ *
44
+ * This matches the object shape Next passes to custom `loader` functions.
45
+ */
42
46
  interface ImageLoaderProps {
43
47
  quality?: number;
44
48
  src: string;
45
49
  width: number;
46
50
  }
47
- /** Configuration for `createStowLoader`. */
51
+ /**
52
+ * Configuration for `createStowLoader()`.
53
+ *
54
+ * Use this to encode a project-wide transformation policy once and then reuse
55
+ * the returned loader across `next/image` components.
56
+ */
48
57
  interface StowLoaderConfig {
49
58
  /**
50
59
  * Fixed aspect ratio (width / height).
@@ -97,8 +106,14 @@ interface StowLoaderConfig {
97
106
  * Create a custom image loader with configuration.
98
107
  *
99
108
  * Returns a stable function reference safe for use as a Next.js `loader` prop.
109
+ *
110
+ * Behavior by source type:
111
+ * - Stow vanity URL: append transform params directly
112
+ * - legacy `/files/...` URL: rewrite to `/transform/...`
113
+ * - external URL with `proxySlug`: route through `proxy.stow.sh`
114
+ * - anything else: return the original source unchanged
100
115
  */
101
- declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality, }: ImageLoaderProps) => string;
116
+ declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality }: ImageLoaderProps) => string;
102
117
  /**
103
118
  * Default Stow image loader.
104
119
  *
@@ -120,6 +135,6 @@ declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, qua
120
135
  * export default stowLoader;
121
136
  * ```
122
137
  */
123
- declare const stowLoader: ({ src, width, quality, }: ImageLoaderProps) => string;
138
+ declare const stowLoader: ({ src, width, quality }: ImageLoaderProps) => string;
124
139
 
125
140
  export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
@@ -38,13 +38,22 @@
38
38
  * }
39
39
  * ```
40
40
  */
41
- /** Next.js image loader input contract. */
41
+ /**
42
+ * Next.js image loader input contract.
43
+ *
44
+ * This matches the object shape Next passes to custom `loader` functions.
45
+ */
42
46
  interface ImageLoaderProps {
43
47
  quality?: number;
44
48
  src: string;
45
49
  width: number;
46
50
  }
47
- /** Configuration for `createStowLoader`. */
51
+ /**
52
+ * Configuration for `createStowLoader()`.
53
+ *
54
+ * Use this to encode a project-wide transformation policy once and then reuse
55
+ * the returned loader across `next/image` components.
56
+ */
48
57
  interface StowLoaderConfig {
49
58
  /**
50
59
  * Fixed aspect ratio (width / height).
@@ -97,8 +106,14 @@ interface StowLoaderConfig {
97
106
  * Create a custom image loader with configuration.
98
107
  *
99
108
  * Returns a stable function reference safe for use as a Next.js `loader` prop.
109
+ *
110
+ * Behavior by source type:
111
+ * - Stow vanity URL: append transform params directly
112
+ * - legacy `/files/...` URL: rewrite to `/transform/...`
113
+ * - external URL with `proxySlug`: route through `proxy.stow.sh`
114
+ * - anything else: return the original source unchanged
100
115
  */
101
- declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality, }: ImageLoaderProps) => string;
116
+ declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality }: ImageLoaderProps) => string;
102
117
  /**
103
118
  * Default Stow image loader.
104
119
  *
@@ -120,6 +135,6 @@ declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, qua
120
135
  * export default stowLoader;
121
136
  * ```
122
137
  */
123
- declare const stowLoader: ({ src, width, quality, }: ImageLoaderProps) => string;
138
+ declare const stowLoader: ({ src, width, quality }: ImageLoaderProps) => string;
124
139
 
125
140
  export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
@@ -53,7 +53,7 @@ function buildTransformParams(width, quality, config) {
53
53
  }
54
54
  function transformStowUrl(src, baseUrl, params) {
55
55
  const url = new URL(src, baseUrl);
56
- const pathname = url.pathname;
56
+ const { pathname } = url;
57
57
  if (pathname.startsWith("/files/")) {
58
58
  const transformPath = pathname.replace("/files/", "/transform/");
59
59
  return `${baseUrl}${transformPath}?${params.toString()}`;
@@ -74,23 +74,15 @@ function createStowLoader(config = {}) {
74
74
  gravity,
75
75
  aspectRatio
76
76
  } = config;
77
- const customDomainSet = new Set(customDomains ?? []);
78
- return function stowLoader2({
79
- src,
80
- width,
81
- quality
82
- }) {
77
+ const customDomainSet = new Set(customDomains);
78
+ return function stowLoader2({ src, width, quality }) {
83
79
  const resolvedQuality = quality || defaultQuality;
84
80
  const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
85
81
  const isAbsoluteUrl = src.startsWith("http://") || src.startsWith("https://");
86
82
  if (customDomainSet.size > 0 && isAbsoluteUrl) {
87
83
  const url = new URL(src);
88
84
  if (customDomainSet.has(url.hostname)) {
89
- const params = buildTransformParams(
90
- width,
91
- resolvedQuality,
92
- paramConfig
93
- );
85
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
94
86
  return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
95
87
  }
96
88
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createStowLoader,
3
3
  stowLoader
4
- } from "./chunk-XJA64ZFQ.mjs";
4
+ } from "./chunk-NKFGLVPF.mjs";
5
5
  export {
6
6
  createStowLoader,
7
7
  stowLoader
package/dist/index.d.mts CHANGED
@@ -23,7 +23,13 @@ export { StowLoaderConfig, createStowLoader, stowLoader } from './image-loader.m
23
23
  * ```
24
24
  */
25
25
 
26
- /** CORS options applied to generated route handlers. */
26
+ /**
27
+ * CORS options applied to generated route handlers.
28
+ *
29
+ * Generated handlers default to browser-friendly CORS because uploads commonly
30
+ * originate from client-side code. Pass `cors: false` to disable header
31
+ * injection entirely or provide a stricter `origin` allowlist for production.
32
+ */
27
33
  interface CorsConfig {
28
34
  /** Allowed headers (default: ["Content-Type"]) */
29
35
  allowedHeaders?: string[];
@@ -35,10 +41,22 @@ interface CorsConfig {
35
41
  origin?: string | string[];
36
42
  }
37
43
  /**
38
- * Create an OPTIONS handler for CORS preflight requests
44
+ * Create an `OPTIONS` handler for CORS preflight requests.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * export const OPTIONS = createCorsPreflightHandler({
49
+ * origin: ["https://app.example.com"],
50
+ * });
51
+ * ```
39
52
  */
40
53
  declare function createCorsPreflightHandler(config?: CorsConfig): () => NextResponse;
41
- /** Minimal server SDK shape required by handler factories. */
54
+ /**
55
+ * Minimal `@howells/stow-server` surface required by the route factories.
56
+ *
57
+ * Accepting this structural type keeps the package test-friendly and allows you
58
+ * to provide a mock implementation without importing the full SDK in every test.
59
+ */
42
60
  interface StowServerLike {
43
61
  confirmUpload: (request: {
44
62
  fileKey: string;
@@ -68,7 +86,12 @@ interface StowServerLike {
68
86
  dedupe?: false;
69
87
  }>;
70
88
  }
71
- /** Options for `createPresignHandler`. */
89
+ /**
90
+ * Options for `createPresignHandler()`.
91
+ *
92
+ * This config defines the validation and routing rules for the first step of
93
+ * the direct-upload flow.
94
+ */
72
95
  interface PresignHandlerConfig {
73
96
  /** Allowed content types (supports wildcards like "image/*") */
74
97
  allowedTypes?: string[];
@@ -76,7 +99,7 @@ interface PresignHandlerConfig {
76
99
  cors?: CorsConfig | false;
77
100
  /** Maximum file size in bytes */
78
101
  maxSize?: number;
79
- /** Default route for organizing files */
102
+ /** Default route/folder hint used when the client does not provide one. */
80
103
  route?: string;
81
104
  /** Stow server instance */
82
105
  stow: StowServerLike;
@@ -87,7 +110,11 @@ interface PresignHandlerConfig {
87
110
  size: number;
88
111
  }) => Promise<boolean | string> | boolean | string;
89
112
  }
90
- /** Options for `createConfirmHandler`. */
113
+ /**
114
+ * Options for `createConfirmHandler()`.
115
+ *
116
+ * This config controls the second step of the direct-upload flow.
117
+ */
91
118
  interface ConfirmHandlerConfig {
92
119
  /** CORS configuration (enabled by default for browser uploads) */
93
120
  cors?: CorsConfig | false;
@@ -105,17 +132,44 @@ interface ConfirmHandlerConfig {
105
132
  * Create a Next.js route handler for presigning uploads.
106
133
  * This is called by @howells/stow-client to get a presigned URL for direct R2 upload.
107
134
  *
108
- * Returns both POST handler and OPTIONS handler for CORS preflight.
135
+ * Export the returned function as your route's `POST` handler. Pair it with
136
+ * `createCorsPreflightHandler()` if you want an explicit `OPTIONS` export.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * import { createCorsPreflightHandler, createPresignHandler } from "@howells/stow-next";
141
+ * import { StowServer } from "@howells/stow-server";
142
+ *
143
+ * const stow = new StowServer({
144
+ * apiKey: process.env.STOW_API_KEY!,
145
+ * bucket: "products",
146
+ * });
147
+ *
148
+ * export const POST = createPresignHandler({
149
+ * stow,
150
+ * allowedTypes: ["image/*"],
151
+ * maxSize: 10 * 1024 * 1024,
152
+ * });
153
+ *
154
+ * export const OPTIONS = createCorsPreflightHandler();
155
+ * ```
109
156
  */
110
157
  declare function createPresignHandler(config: PresignHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
111
158
  /**
112
159
  * Create a Next.js route handler for confirming uploads.
113
160
  * This is called by @howells/stow-client after uploading to R2.
114
161
  *
115
- * Returns both POST handler and OPTIONS handler for CORS preflight.
162
+ * Export the returned function as your route's `POST` handler. Use
163
+ * `onUploadComplete` for app-specific follow-up work like cache invalidation or
164
+ * analytics.
116
165
  */
117
166
  declare function createConfirmHandler(config: ConfirmHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
118
- /** Minimal combined shape for single-route upload handlers. */
167
+ /**
168
+ * Minimal combined shape for single-route upload handlers.
169
+ *
170
+ * This legacy path proxies file bytes through your Next.js route instead of
171
+ * using browser-to-storage uploads.
172
+ */
119
173
  interface StowLike {
120
174
  uploadFile: (file: Blob, options?: {
121
175
  filename?: string;
@@ -128,7 +182,11 @@ interface StowLike {
128
182
  contentType: string;
129
183
  }>;
130
184
  }
131
- /** Options for `createUploadHandler`. */
185
+ /**
186
+ * Options for the deprecated `createUploadHandler()`.
187
+ *
188
+ * Prefer `createPresignHandler()` plus `createConfirmHandler()` for new work.
189
+ */
132
190
  interface UploadHandlerConfig {
133
191
  /** Allowed content types (supports wildcards like "image/*") */
134
192
  allowedTypes?: string[];
@@ -155,6 +213,9 @@ interface UploadHandlerConfig {
155
213
  *
156
214
  * @deprecated Use createPresignHandler + createConfirmHandler for direct uploads.
157
215
  * Proxied uploads are limited by serverless payload limits (~4.5MB).
216
+ *
217
+ * This remains useful for quick internal tools or environments where a single
218
+ * multipart endpoint is acceptable and file sizes are small.
158
219
  */
159
220
  declare function createUploadHandler(config: UploadHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
160
221
 
package/dist/index.d.ts CHANGED
@@ -23,7 +23,13 @@ export { StowLoaderConfig, createStowLoader, stowLoader } from './image-loader.j
23
23
  * ```
24
24
  */
25
25
 
26
- /** CORS options applied to generated route handlers. */
26
+ /**
27
+ * CORS options applied to generated route handlers.
28
+ *
29
+ * Generated handlers default to browser-friendly CORS because uploads commonly
30
+ * originate from client-side code. Pass `cors: false` to disable header
31
+ * injection entirely or provide a stricter `origin` allowlist for production.
32
+ */
27
33
  interface CorsConfig {
28
34
  /** Allowed headers (default: ["Content-Type"]) */
29
35
  allowedHeaders?: string[];
@@ -35,10 +41,22 @@ interface CorsConfig {
35
41
  origin?: string | string[];
36
42
  }
37
43
  /**
38
- * Create an OPTIONS handler for CORS preflight requests
44
+ * Create an `OPTIONS` handler for CORS preflight requests.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * export const OPTIONS = createCorsPreflightHandler({
49
+ * origin: ["https://app.example.com"],
50
+ * });
51
+ * ```
39
52
  */
40
53
  declare function createCorsPreflightHandler(config?: CorsConfig): () => NextResponse;
41
- /** Minimal server SDK shape required by handler factories. */
54
+ /**
55
+ * Minimal `@howells/stow-server` surface required by the route factories.
56
+ *
57
+ * Accepting this structural type keeps the package test-friendly and allows you
58
+ * to provide a mock implementation without importing the full SDK in every test.
59
+ */
42
60
  interface StowServerLike {
43
61
  confirmUpload: (request: {
44
62
  fileKey: string;
@@ -68,7 +86,12 @@ interface StowServerLike {
68
86
  dedupe?: false;
69
87
  }>;
70
88
  }
71
- /** Options for `createPresignHandler`. */
89
+ /**
90
+ * Options for `createPresignHandler()`.
91
+ *
92
+ * This config defines the validation and routing rules for the first step of
93
+ * the direct-upload flow.
94
+ */
72
95
  interface PresignHandlerConfig {
73
96
  /** Allowed content types (supports wildcards like "image/*") */
74
97
  allowedTypes?: string[];
@@ -76,7 +99,7 @@ interface PresignHandlerConfig {
76
99
  cors?: CorsConfig | false;
77
100
  /** Maximum file size in bytes */
78
101
  maxSize?: number;
79
- /** Default route for organizing files */
102
+ /** Default route/folder hint used when the client does not provide one. */
80
103
  route?: string;
81
104
  /** Stow server instance */
82
105
  stow: StowServerLike;
@@ -87,7 +110,11 @@ interface PresignHandlerConfig {
87
110
  size: number;
88
111
  }) => Promise<boolean | string> | boolean | string;
89
112
  }
90
- /** Options for `createConfirmHandler`. */
113
+ /**
114
+ * Options for `createConfirmHandler()`.
115
+ *
116
+ * This config controls the second step of the direct-upload flow.
117
+ */
91
118
  interface ConfirmHandlerConfig {
92
119
  /** CORS configuration (enabled by default for browser uploads) */
93
120
  cors?: CorsConfig | false;
@@ -105,17 +132,44 @@ interface ConfirmHandlerConfig {
105
132
  * Create a Next.js route handler for presigning uploads.
106
133
  * This is called by @howells/stow-client to get a presigned URL for direct R2 upload.
107
134
  *
108
- * Returns both POST handler and OPTIONS handler for CORS preflight.
135
+ * Export the returned function as your route's `POST` handler. Pair it with
136
+ * `createCorsPreflightHandler()` if you want an explicit `OPTIONS` export.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * import { createCorsPreflightHandler, createPresignHandler } from "@howells/stow-next";
141
+ * import { StowServer } from "@howells/stow-server";
142
+ *
143
+ * const stow = new StowServer({
144
+ * apiKey: process.env.STOW_API_KEY!,
145
+ * bucket: "products",
146
+ * });
147
+ *
148
+ * export const POST = createPresignHandler({
149
+ * stow,
150
+ * allowedTypes: ["image/*"],
151
+ * maxSize: 10 * 1024 * 1024,
152
+ * });
153
+ *
154
+ * export const OPTIONS = createCorsPreflightHandler();
155
+ * ```
109
156
  */
110
157
  declare function createPresignHandler(config: PresignHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
111
158
  /**
112
159
  * Create a Next.js route handler for confirming uploads.
113
160
  * This is called by @howells/stow-client after uploading to R2.
114
161
  *
115
- * Returns both POST handler and OPTIONS handler for CORS preflight.
162
+ * Export the returned function as your route's `POST` handler. Use
163
+ * `onUploadComplete` for app-specific follow-up work like cache invalidation or
164
+ * analytics.
116
165
  */
117
166
  declare function createConfirmHandler(config: ConfirmHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
118
- /** Minimal combined shape for single-route upload handlers. */
167
+ /**
168
+ * Minimal combined shape for single-route upload handlers.
169
+ *
170
+ * This legacy path proxies file bytes through your Next.js route instead of
171
+ * using browser-to-storage uploads.
172
+ */
119
173
  interface StowLike {
120
174
  uploadFile: (file: Blob, options?: {
121
175
  filename?: string;
@@ -128,7 +182,11 @@ interface StowLike {
128
182
  contentType: string;
129
183
  }>;
130
184
  }
131
- /** Options for `createUploadHandler`. */
185
+ /**
186
+ * Options for the deprecated `createUploadHandler()`.
187
+ *
188
+ * Prefer `createPresignHandler()` plus `createConfirmHandler()` for new work.
189
+ */
132
190
  interface UploadHandlerConfig {
133
191
  /** Allowed content types (supports wildcards like "image/*") */
134
192
  allowedTypes?: string[];
@@ -155,6 +213,9 @@ interface UploadHandlerConfig {
155
213
  *
156
214
  * @deprecated Use createPresignHandler + createConfirmHandler for direct uploads.
157
215
  * Proxied uploads are limited by serverless payload limits (~4.5MB).
216
+ *
217
+ * This remains useful for quick internal tools or environments where a single
218
+ * multipart endpoint is acceptable and file sizes are small.
158
219
  */
159
220
  declare function createUploadHandler(config: UploadHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
160
221
 
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ function buildTransformParams(width, quality, config) {
60
60
  }
61
61
  function transformStowUrl(src, baseUrl, params) {
62
62
  const url = new URL(src, baseUrl);
63
- const pathname = url.pathname;
63
+ const { pathname } = url;
64
64
  if (pathname.startsWith("/files/")) {
65
65
  const transformPath = pathname.replace("/files/", "/transform/");
66
66
  return `${baseUrl}${transformPath}?${params.toString()}`;
@@ -81,23 +81,15 @@ function createStowLoader(config = {}) {
81
81
  gravity,
82
82
  aspectRatio
83
83
  } = config;
84
- const customDomainSet = new Set(customDomains ?? []);
85
- return function stowLoader2({
86
- src,
87
- width,
88
- quality
89
- }) {
84
+ const customDomainSet = new Set(customDomains);
85
+ return function stowLoader2({ src, width, quality }) {
90
86
  const resolvedQuality = quality || defaultQuality;
91
87
  const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
92
88
  const isAbsoluteUrl = src.startsWith("http://") || src.startsWith("https://");
93
89
  if (customDomainSet.size > 0 && isAbsoluteUrl) {
94
90
  const url = new URL(src);
95
91
  if (customDomainSet.has(url.hostname)) {
96
- const params = buildTransformParams(
97
- width,
98
- resolvedQuality,
99
- paramConfig
100
- );
92
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
101
93
  return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
102
94
  }
103
95
  }
@@ -147,15 +139,25 @@ function getLocalConfirmPath(pathname) {
147
139
  }
148
140
  return pathname.endsWith("/") ? `${pathname}confirm` : `${pathname}/confirm`;
149
141
  }
142
+ function formatBytes(bytes) {
143
+ if (bytes === 0) {
144
+ return "0 Bytes";
145
+ }
146
+ const k = 1024;
147
+ const sizes = ["Bytes", "KB", "MB", "GB"];
148
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
149
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
150
+ }
151
+ var MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9.+-]+$/i;
152
+ function isValidMimeType(mimeType) {
153
+ return MIME_TYPE_PATTERN.test(mimeType);
154
+ }
150
155
  function withCors(response, config) {
151
156
  const cors = { ...DEFAULT_CORS, ...config };
152
157
  const origin = Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin;
153
158
  response.headers.set("Access-Control-Allow-Origin", origin);
154
159
  response.headers.set("Access-Control-Allow-Methods", cors.methods.join(", "));
155
- response.headers.set(
156
- "Access-Control-Allow-Headers",
157
- cors.allowedHeaders.join(", ")
158
- );
160
+ response.headers.set("Access-Control-Allow-Headers", cors.allowedHeaders.join(", "));
159
161
  response.headers.set("Access-Control-Max-Age", cors.maxAge.toString());
160
162
  return response;
161
163
  }
@@ -205,10 +207,7 @@ function createPresignHandler(config) {
205
207
  return contentType === type;
206
208
  });
207
209
  if (!isAllowed) {
208
- return respond(
209
- { error: `File type "${contentType}" is not allowed` },
210
- 400
211
- );
210
+ return respond({ error: `File type "${contentType}" is not allowed` }, 400);
212
211
  }
213
212
  }
214
213
  if (config.validate) {
@@ -246,7 +245,7 @@ function createPresignHandler(config) {
246
245
  } catch (error) {
247
246
  console.error("Presign error:", error);
248
247
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
249
- const status = error.status;
248
+ const { status } = error;
250
249
  return respond({ error: error.message }, status >= 400 ? status : 500);
251
250
  }
252
251
  return respond({ error: "Presign failed" }, 500);
@@ -291,7 +290,7 @@ function createConfirmHandler(config) {
291
290
  } catch (error) {
292
291
  console.error("Confirm error:", error);
293
292
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
294
- const status = error.status;
293
+ const { status } = error;
295
294
  return respond({ error: error.message }, status >= 400 ? status : 500);
296
295
  }
297
296
  return respond({ error: "Confirm failed" }, 500);
@@ -313,10 +312,7 @@ function createUploadHandler(config) {
313
312
  const file = formData.get("file");
314
313
  const route = formData.get("route") || config.route;
315
314
  if (!file) {
316
- return import_server.NextResponse.json(
317
- { error: "No file provided" },
318
- { status: 400 }
319
- );
315
+ return import_server.NextResponse.json({ error: "No file provided" }, { status: 400 });
320
316
  }
321
317
  if (config.maxSize && file.size > config.maxSize) {
322
318
  return import_server.NextResponse.json(
@@ -363,7 +359,7 @@ function createUploadHandler(config) {
363
359
  } catch (error) {
364
360
  console.error("Upload error:", error);
365
361
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
366
- const status = error.status;
362
+ const { status } = error;
367
363
  return import_server.NextResponse.json(
368
364
  { error: error.message },
369
365
  { status: status >= 400 ? status : 500 }
@@ -373,19 +369,6 @@ function createUploadHandler(config) {
373
369
  }
374
370
  };
375
371
  }
376
- function formatBytes(bytes) {
377
- if (bytes === 0) {
378
- return "0 Bytes";
379
- }
380
- const k = 1024;
381
- const sizes = ["Bytes", "KB", "MB", "GB"];
382
- const i = Math.floor(Math.log(bytes) / Math.log(k));
383
- return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
384
- }
385
- var MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9.+-]+$/i;
386
- function isValidMimeType(mimeType) {
387
- return MIME_TYPE_PATTERN.test(mimeType);
388
- }
389
372
  // Annotate the CommonJS export names for ESM import in node:
390
373
  0 && (module.exports = {
391
374
  createConfirmHandler,
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createStowLoader,
3
3
  stowLoader
4
- } from "./chunk-XJA64ZFQ.mjs";
4
+ } from "./chunk-NKFGLVPF.mjs";
5
5
 
6
6
  // src/index.ts
7
7
  import { NextResponse } from "next/server";
@@ -18,15 +18,25 @@ function getLocalConfirmPath(pathname) {
18
18
  }
19
19
  return pathname.endsWith("/") ? `${pathname}confirm` : `${pathname}/confirm`;
20
20
  }
21
+ function formatBytes(bytes) {
22
+ if (bytes === 0) {
23
+ return "0 Bytes";
24
+ }
25
+ const k = 1024;
26
+ const sizes = ["Bytes", "KB", "MB", "GB"];
27
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
28
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
29
+ }
30
+ var MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9.+-]+$/i;
31
+ function isValidMimeType(mimeType) {
32
+ return MIME_TYPE_PATTERN.test(mimeType);
33
+ }
21
34
  function withCors(response, config) {
22
35
  const cors = { ...DEFAULT_CORS, ...config };
23
36
  const origin = Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin;
24
37
  response.headers.set("Access-Control-Allow-Origin", origin);
25
38
  response.headers.set("Access-Control-Allow-Methods", cors.methods.join(", "));
26
- response.headers.set(
27
- "Access-Control-Allow-Headers",
28
- cors.allowedHeaders.join(", ")
29
- );
39
+ response.headers.set("Access-Control-Allow-Headers", cors.allowedHeaders.join(", "));
30
40
  response.headers.set("Access-Control-Max-Age", cors.maxAge.toString());
31
41
  return response;
32
42
  }
@@ -76,10 +86,7 @@ function createPresignHandler(config) {
76
86
  return contentType === type;
77
87
  });
78
88
  if (!isAllowed) {
79
- return respond(
80
- { error: `File type "${contentType}" is not allowed` },
81
- 400
82
- );
89
+ return respond({ error: `File type "${contentType}" is not allowed` }, 400);
83
90
  }
84
91
  }
85
92
  if (config.validate) {
@@ -117,7 +124,7 @@ function createPresignHandler(config) {
117
124
  } catch (error) {
118
125
  console.error("Presign error:", error);
119
126
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
120
- const status = error.status;
127
+ const { status } = error;
121
128
  return respond({ error: error.message }, status >= 400 ? status : 500);
122
129
  }
123
130
  return respond({ error: "Presign failed" }, 500);
@@ -162,7 +169,7 @@ function createConfirmHandler(config) {
162
169
  } catch (error) {
163
170
  console.error("Confirm error:", error);
164
171
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
165
- const status = error.status;
172
+ const { status } = error;
166
173
  return respond({ error: error.message }, status >= 400 ? status : 500);
167
174
  }
168
175
  return respond({ error: "Confirm failed" }, 500);
@@ -184,10 +191,7 @@ function createUploadHandler(config) {
184
191
  const file = formData.get("file");
185
192
  const route = formData.get("route") || config.route;
186
193
  if (!file) {
187
- return NextResponse.json(
188
- { error: "No file provided" },
189
- { status: 400 }
190
- );
194
+ return NextResponse.json({ error: "No file provided" }, { status: 400 });
191
195
  }
192
196
  if (config.maxSize && file.size > config.maxSize) {
193
197
  return NextResponse.json(
@@ -234,7 +238,7 @@ function createUploadHandler(config) {
234
238
  } catch (error) {
235
239
  console.error("Upload error:", error);
236
240
  if (error instanceof Error && "status" in error && typeof error.status === "number") {
237
- const status = error.status;
241
+ const { status } = error;
238
242
  return NextResponse.json(
239
243
  { error: error.message },
240
244
  { status: status >= 400 ? status : 500 }
@@ -244,19 +248,6 @@ function createUploadHandler(config) {
244
248
  }
245
249
  };
246
250
  }
247
- function formatBytes(bytes) {
248
- if (bytes === 0) {
249
- return "0 Bytes";
250
- }
251
- const k = 1024;
252
- const sizes = ["Bytes", "KB", "MB", "GB"];
253
- const i = Math.floor(Math.log(bytes) / Math.log(k));
254
- return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
255
- }
256
- var MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9.+-]+$/i;
257
- function isValidMimeType(mimeType) {
258
- return MIME_TYPE_PATTERN.test(mimeType);
259
- }
260
251
  export {
261
252
  createConfirmHandler,
262
253
  createCorsPreflightHandler,
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@howells/stow-next",
3
- "version": "2.1.0",
3
+ "version": "2.4.0",
4
4
  "description": "Next.js integration for Stow file storage",
5
+ "keywords": [
6
+ "file-storage",
7
+ "image-loader",
8
+ "nextjs",
9
+ "sdk",
10
+ "stow"
11
+ ],
12
+ "homepage": "https://stow.sh",
5
13
  "license": "MIT",
6
14
  "repository": {
7
15
  "type": "git",
8
16
  "url": "git+https://github.com/howells/stow.git",
9
17
  "directory": "packages/stow-next"
10
18
  },
11
- "homepage": "https://stow.sh",
12
- "keywords": [
13
- "stow",
14
- "file-storage",
15
- "nextjs",
16
- "image-loader",
17
- "sdk"
19
+ "files": [
20
+ "dist"
18
21
  ],
19
22
  "main": "./dist/index.js",
20
23
  "module": "./dist/index.mjs",
@@ -31,26 +34,23 @@
31
34
  "require": "./dist/image-loader.js"
32
35
  }
33
36
  },
34
- "files": [
35
- "dist"
36
- ],
37
- "peerDependencies": {
38
- "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
39
- "react": "^18.0.0 || ^19.0.0"
40
- },
41
37
  "devDependencies": {
42
38
  "@types/react": "19.2.14",
43
- "next": "16.1.7",
39
+ "next": "16.2.0",
44
40
  "react": "^19.2.4",
45
41
  "tsup": "^8.5.1",
46
42
  "typescript": "^5.9.3",
47
- "vitest": "^4.1.0",
43
+ "vite-plus": "latest",
48
44
  "@stow/typescript-config": "0.0.0"
49
45
  },
46
+ "peerDependencies": {
47
+ "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
48
+ "react": "^18.0.0 || ^19.0.0"
49
+ },
50
50
  "scripts": {
51
51
  "build": "tsup src/index.ts src/image-loader.ts --format cjs,esm --dts --external next --external react",
52
52
  "dev": "tsup src/index.ts src/image-loader.ts --format cjs,esm --dts --external next --external react --watch",
53
- "test": "vitest run",
54
- "test:watch": "vitest"
53
+ "test": "vp test run",
54
+ "test:watch": "vp test"
55
55
  }
56
56
  }