@howells/stow-next 2.2.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 +5 -13
- package/dist/chunk-NKFGLVPF.mjs +99 -0
- package/dist/image-loader.d.mts +19 -4
- package/dist/image-loader.d.ts +19 -4
- package/dist/image-loader.js +4 -12
- package/dist/image-loader.mjs +1 -1
- package/dist/index.d.mts +71 -10
- package/dist/index.d.ts +71 -10
- package/dist/index.js +23 -40
- package/dist/index.mjs +20 -29
- package/package.json +19 -19
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,
|
|
64
|
-
allowedTypes: ["image/*", ".pdf"],
|
|
65
|
-
route: "uploads",
|
|
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
|
+
};
|
package/dist/image-loader.d.mts
CHANGED
|
@@ -38,13 +38,22 @@
|
|
|
38
38
|
* }
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
138
|
+
declare const stowLoader: ({ src, width, quality }: ImageLoaderProps) => string;
|
|
124
139
|
|
|
125
140
|
export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
|
package/dist/image-loader.d.ts
CHANGED
|
@@ -38,13 +38,22 @@
|
|
|
38
38
|
* }
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
138
|
+
declare const stowLoader: ({ src, width, quality }: ImageLoaderProps) => string;
|
|
124
139
|
|
|
125
140
|
export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
|
package/dist/image-loader.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/image-loader.mjs
CHANGED
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
12
|
-
|
|
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.
|
|
39
|
+
"next": "16.2.0",
|
|
44
40
|
"react": "^19.2.4",
|
|
45
41
|
"tsup": "^8.5.1",
|
|
46
42
|
"typescript": "^5.9.3",
|
|
47
|
-
"
|
|
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": "
|
|
54
|
-
"test:watch": "
|
|
53
|
+
"test": "vp test run",
|
|
54
|
+
"test:watch": "vp test"
|
|
55
55
|
}
|
|
56
56
|
}
|