@howells/stow-next 0.1.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/dist/index.js ADDED
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createConfirmHandler: () => createConfirmHandler,
24
+ createCorsPreflightHandler: () => createCorsPreflightHandler,
25
+ createPresignHandler: () => createPresignHandler,
26
+ createStowLoader: () => createStowLoader,
27
+ createUploadHandler: () => createUploadHandler,
28
+ stowLoader: () => stowLoader
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_server = require("next/server");
32
+
33
+ // src/image-loader.ts
34
+ var STOW_DOMAIN_PATTERN = /\.stow\.sh$/;
35
+ function isStowUrl(src) {
36
+ try {
37
+ const url = new URL(src);
38
+ return STOW_DOMAIN_PATTERN.test(url.hostname);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function buildTransformParams(width, quality, config) {
44
+ const params = new URLSearchParams();
45
+ params.set("w", width.toString());
46
+ if (config.aspectRatio) {
47
+ params.set("h", Math.round(width / config.aspectRatio).toString());
48
+ }
49
+ params.set("q", quality.toString());
50
+ if (config.fit) {
51
+ params.set("fit", config.fit);
52
+ }
53
+ if (config.gravity) {
54
+ params.set("gravity", config.gravity);
55
+ }
56
+ if (config.defaultFormat) {
57
+ params.set("f", config.defaultFormat);
58
+ }
59
+ return params;
60
+ }
61
+ function transformStowUrl(src, baseUrl, params) {
62
+ const url = new URL(src, baseUrl);
63
+ const pathname = url.pathname;
64
+ if (pathname.startsWith("/files/")) {
65
+ const transformPath = pathname.replace("/files/", "/transform/");
66
+ return `${baseUrl}${transformPath}?${params.toString()}`;
67
+ }
68
+ if (pathname.startsWith("/transform/")) {
69
+ return `${baseUrl}${pathname}?${params.toString()}`;
70
+ }
71
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
72
+ }
73
+ function createStowLoader(config = {}) {
74
+ const {
75
+ baseUrl = "https://stow.sh",
76
+ defaultQuality = 75,
77
+ defaultFormat,
78
+ proxySlug,
79
+ fit,
80
+ gravity,
81
+ aspectRatio
82
+ } = config;
83
+ return function stowLoader2({
84
+ src,
85
+ width,
86
+ quality
87
+ }) {
88
+ const resolvedQuality = quality || defaultQuality;
89
+ const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
90
+ if (isStowUrl(src)) {
91
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
92
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
93
+ }
94
+ if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
95
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
96
+ return transformStowUrl(src, baseUrl, params);
97
+ }
98
+ if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
99
+ const params = new URLSearchParams();
100
+ params.set("url", src);
101
+ params.set("w", width.toString());
102
+ if (aspectRatio) {
103
+ params.set("h", Math.round(width / aspectRatio).toString());
104
+ }
105
+ params.set("q", resolvedQuality.toString());
106
+ if (fit) {
107
+ params.set("fit", fit);
108
+ }
109
+ if (gravity) {
110
+ params.set("gravity", gravity);
111
+ }
112
+ if (defaultFormat) {
113
+ params.set("f", defaultFormat);
114
+ }
115
+ return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
116
+ }
117
+ return src;
118
+ };
119
+ }
120
+ var stowLoader = createStowLoader();
121
+
122
+ // src/index.ts
123
+ var DEFAULT_CORS = {
124
+ origin: "*",
125
+ methods: ["POST", "OPTIONS"],
126
+ allowedHeaders: ["Content-Type"],
127
+ maxAge: 86400
128
+ };
129
+ function getLocalConfirmPath(pathname) {
130
+ if (pathname.endsWith("/presign")) {
131
+ const PRESIGN_SUFFIX_LENGTH = 7;
132
+ return `${pathname.slice(0, -PRESIGN_SUFFIX_LENGTH)}confirm`;
133
+ }
134
+ return pathname.endsWith("/") ? `${pathname}confirm` : `${pathname}/confirm`;
135
+ }
136
+ function withCors(response, config) {
137
+ const cors = { ...DEFAULT_CORS, ...config };
138
+ const origin = Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin;
139
+ response.headers.set("Access-Control-Allow-Origin", origin);
140
+ response.headers.set("Access-Control-Allow-Methods", cors.methods.join(", "));
141
+ response.headers.set(
142
+ "Access-Control-Allow-Headers",
143
+ cors.allowedHeaders.join(", ")
144
+ );
145
+ response.headers.set("Access-Control-Max-Age", cors.maxAge.toString());
146
+ return response;
147
+ }
148
+ function createCorsPreflightHandler(config) {
149
+ return function handler() {
150
+ return withCors(new import_server.NextResponse(null, { status: 204 }), config);
151
+ };
152
+ }
153
+ function createPresignHandler(config) {
154
+ const corsEnabled = config.cors !== false;
155
+ const corsConfig = config.cors === false ? void 0 : config.cors;
156
+ const respond = (data, status) => {
157
+ const response = import_server.NextResponse.json(data, { status });
158
+ return corsEnabled ? withCors(response, corsConfig) : response;
159
+ };
160
+ async function handler(request) {
161
+ if (request.method === "OPTIONS") {
162
+ const response = new import_server.NextResponse(null, { status: 204 });
163
+ return corsEnabled ? withCors(response, corsConfig) : response;
164
+ }
165
+ try {
166
+ const body = await request.json();
167
+ const { filename, contentType, size, route: requestRoute } = body;
168
+ if (!filename || typeof filename !== "string") {
169
+ return respond({ error: "filename is required" }, 400);
170
+ }
171
+ if (!contentType || typeof contentType !== "string") {
172
+ return respond({ error: "contentType is required" }, 400);
173
+ }
174
+ if (typeof size !== "number" || size <= 0) {
175
+ return respond({ error: "size must be a positive number" }, 400);
176
+ }
177
+ if (config.maxSize && size > config.maxSize) {
178
+ return respond(
179
+ {
180
+ error: `File too large. Maximum size is ${formatBytes(config.maxSize)}`
181
+ },
182
+ 400
183
+ );
184
+ }
185
+ if (config.allowedTypes && config.allowedTypes.length > 0) {
186
+ const isAllowed = config.allowedTypes.some((type) => {
187
+ if (type.endsWith("/*")) {
188
+ const prefix = type.slice(0, -1);
189
+ return contentType.startsWith(prefix);
190
+ }
191
+ return contentType === type;
192
+ });
193
+ if (!isAllowed) {
194
+ return respond(
195
+ { error: `File type "${contentType}" is not allowed` },
196
+ 400
197
+ );
198
+ }
199
+ }
200
+ if (config.validate) {
201
+ const validationResult = await config.validate({
202
+ filename,
203
+ contentType,
204
+ size
205
+ });
206
+ if (validationResult !== true) {
207
+ return respond(
208
+ {
209
+ error: typeof validationResult === "string" ? validationResult : "Validation failed"
210
+ },
211
+ 400
212
+ );
213
+ }
214
+ }
215
+ const route = requestRoute || config.route;
216
+ const result = await config.stow.getPresignedUrl({
217
+ filename,
218
+ contentType,
219
+ size,
220
+ route
221
+ });
222
+ if ("dedupe" in result && result.dedupe) {
223
+ return respond(result, 200);
224
+ }
225
+ return respond(
226
+ {
227
+ ...result,
228
+ confirmUrl: getLocalConfirmPath(request.nextUrl.pathname)
229
+ },
230
+ 200
231
+ );
232
+ } catch (error) {
233
+ console.error("Presign error:", error);
234
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
235
+ const status = error.status;
236
+ return respond({ error: error.message }, status >= 400 ? status : 500);
237
+ }
238
+ return respond({ error: "Presign failed" }, 500);
239
+ }
240
+ }
241
+ return handler;
242
+ }
243
+ function createConfirmHandler(config) {
244
+ const corsEnabled = config.cors !== false;
245
+ const corsConfig = config.cors === false ? void 0 : config.cors;
246
+ const respond = (data, status) => {
247
+ const response = import_server.NextResponse.json(data, { status });
248
+ return corsEnabled ? withCors(response, corsConfig) : response;
249
+ };
250
+ async function handler(request) {
251
+ if (request.method === "OPTIONS") {
252
+ const response = new import_server.NextResponse(null, { status: 204 });
253
+ return corsEnabled ? withCors(response, corsConfig) : response;
254
+ }
255
+ try {
256
+ const body = await request.json();
257
+ const { fileKey, size, contentType } = body;
258
+ if (!fileKey || typeof fileKey !== "string") {
259
+ return respond({ error: "fileKey is required" }, 400);
260
+ }
261
+ if (typeof size !== "number" || size <= 0) {
262
+ return respond({ error: "size must be a positive number" }, 400);
263
+ }
264
+ if (!contentType || typeof contentType !== "string") {
265
+ return respond({ error: "contentType is required" }, 400);
266
+ }
267
+ if (!isValidMimeType(contentType)) {
268
+ return respond({ error: "Invalid contentType format" }, 400);
269
+ }
270
+ const result = await config.stow.confirmUpload({
271
+ fileKey,
272
+ size,
273
+ contentType
274
+ });
275
+ await config.onUploadComplete?.(result);
276
+ return respond(result, 200);
277
+ } catch (error) {
278
+ console.error("Confirm error:", error);
279
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
280
+ const status = error.status;
281
+ return respond({ error: error.message }, status >= 400 ? status : 500);
282
+ }
283
+ return respond({ error: "Confirm failed" }, 500);
284
+ }
285
+ }
286
+ return handler;
287
+ }
288
+ function createUploadHandler(config) {
289
+ let warned = false;
290
+ return async function handler(request) {
291
+ if (!warned) {
292
+ console.warn(
293
+ "[@howells/stow-next] createUploadHandler is deprecated. Use createPresignHandler + createConfirmHandler for direct uploads. Proxied uploads are limited by serverless payload limits (~4.5MB on Vercel)."
294
+ );
295
+ warned = true;
296
+ }
297
+ try {
298
+ const formData = await request.formData();
299
+ const file = formData.get("file");
300
+ const route = formData.get("route") || config.route;
301
+ if (!file) {
302
+ return import_server.NextResponse.json(
303
+ { error: "No file provided" },
304
+ { status: 400 }
305
+ );
306
+ }
307
+ if (config.maxSize && file.size > config.maxSize) {
308
+ return import_server.NextResponse.json(
309
+ {
310
+ error: `File too large. Maximum size is ${formatBytes(config.maxSize)}`
311
+ },
312
+ { status: 400 }
313
+ );
314
+ }
315
+ if (config.allowedTypes && config.allowedTypes.length > 0) {
316
+ const isAllowed = config.allowedTypes.some((type) => {
317
+ if (type.endsWith("/*")) {
318
+ const prefix = type.slice(0, -1);
319
+ return file.type.startsWith(prefix);
320
+ }
321
+ return file.type === type;
322
+ });
323
+ if (!isAllowed) {
324
+ return import_server.NextResponse.json(
325
+ { error: `File type "${file.type}" is not allowed` },
326
+ { status: 400 }
327
+ );
328
+ }
329
+ }
330
+ if (config.validate) {
331
+ const validationResult = await config.validate(file);
332
+ if (validationResult !== true) {
333
+ return import_server.NextResponse.json(
334
+ {
335
+ error: typeof validationResult === "string" ? validationResult : "File validation failed"
336
+ },
337
+ { status: 400 }
338
+ );
339
+ }
340
+ }
341
+ await config.onUploadBegin?.(file);
342
+ const result = await config.stow.uploadFile(file, {
343
+ filename: file.name,
344
+ contentType: file.type,
345
+ route
346
+ });
347
+ await config.onUploadComplete?.(result);
348
+ return import_server.NextResponse.json(result);
349
+ } catch (error) {
350
+ console.error("Upload error:", error);
351
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
352
+ const status = error.status;
353
+ return import_server.NextResponse.json(
354
+ { error: error.message },
355
+ { status: status >= 400 ? status : 500 }
356
+ );
357
+ }
358
+ return import_server.NextResponse.json({ error: "Upload failed" }, { status: 500 });
359
+ }
360
+ };
361
+ }
362
+ function formatBytes(bytes) {
363
+ if (bytes === 0) {
364
+ return "0 Bytes";
365
+ }
366
+ const k = 1024;
367
+ const sizes = ["Bytes", "KB", "MB", "GB"];
368
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
369
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
370
+ }
371
+ var MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9.+-]+$/i;
372
+ function isValidMimeType(mimeType) {
373
+ return MIME_TYPE_PATTERN.test(mimeType);
374
+ }
375
+ // Annotate the CommonJS export names for ESM import in node:
376
+ 0 && (module.exports = {
377
+ createConfirmHandler,
378
+ createCorsPreflightHandler,
379
+ createPresignHandler,
380
+ createStowLoader,
381
+ createUploadHandler,
382
+ stowLoader
383
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,267 @@
1
+ import {
2
+ createStowLoader,
3
+ stowLoader
4
+ } from "./chunk-WN3NWAEN.mjs";
5
+
6
+ // src/index.ts
7
+ import { NextResponse } from "next/server";
8
+ var DEFAULT_CORS = {
9
+ origin: "*",
10
+ methods: ["POST", "OPTIONS"],
11
+ allowedHeaders: ["Content-Type"],
12
+ maxAge: 86400
13
+ };
14
+ function getLocalConfirmPath(pathname) {
15
+ if (pathname.endsWith("/presign")) {
16
+ const PRESIGN_SUFFIX_LENGTH = 7;
17
+ return `${pathname.slice(0, -PRESIGN_SUFFIX_LENGTH)}confirm`;
18
+ }
19
+ return pathname.endsWith("/") ? `${pathname}confirm` : `${pathname}/confirm`;
20
+ }
21
+ function withCors(response, config) {
22
+ const cors = { ...DEFAULT_CORS, ...config };
23
+ const origin = Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin;
24
+ response.headers.set("Access-Control-Allow-Origin", origin);
25
+ response.headers.set("Access-Control-Allow-Methods", cors.methods.join(", "));
26
+ response.headers.set(
27
+ "Access-Control-Allow-Headers",
28
+ cors.allowedHeaders.join(", ")
29
+ );
30
+ response.headers.set("Access-Control-Max-Age", cors.maxAge.toString());
31
+ return response;
32
+ }
33
+ function createCorsPreflightHandler(config) {
34
+ return function handler() {
35
+ return withCors(new NextResponse(null, { status: 204 }), config);
36
+ };
37
+ }
38
+ function createPresignHandler(config) {
39
+ const corsEnabled = config.cors !== false;
40
+ const corsConfig = config.cors === false ? void 0 : config.cors;
41
+ const respond = (data, status) => {
42
+ const response = NextResponse.json(data, { status });
43
+ return corsEnabled ? withCors(response, corsConfig) : response;
44
+ };
45
+ async function handler(request) {
46
+ if (request.method === "OPTIONS") {
47
+ const response = new NextResponse(null, { status: 204 });
48
+ return corsEnabled ? withCors(response, corsConfig) : response;
49
+ }
50
+ try {
51
+ const body = await request.json();
52
+ const { filename, contentType, size, route: requestRoute } = body;
53
+ if (!filename || typeof filename !== "string") {
54
+ return respond({ error: "filename is required" }, 400);
55
+ }
56
+ if (!contentType || typeof contentType !== "string") {
57
+ return respond({ error: "contentType is required" }, 400);
58
+ }
59
+ if (typeof size !== "number" || size <= 0) {
60
+ return respond({ error: "size must be a positive number" }, 400);
61
+ }
62
+ if (config.maxSize && size > config.maxSize) {
63
+ return respond(
64
+ {
65
+ error: `File too large. Maximum size is ${formatBytes(config.maxSize)}`
66
+ },
67
+ 400
68
+ );
69
+ }
70
+ if (config.allowedTypes && config.allowedTypes.length > 0) {
71
+ const isAllowed = config.allowedTypes.some((type) => {
72
+ if (type.endsWith("/*")) {
73
+ const prefix = type.slice(0, -1);
74
+ return contentType.startsWith(prefix);
75
+ }
76
+ return contentType === type;
77
+ });
78
+ if (!isAllowed) {
79
+ return respond(
80
+ { error: `File type "${contentType}" is not allowed` },
81
+ 400
82
+ );
83
+ }
84
+ }
85
+ if (config.validate) {
86
+ const validationResult = await config.validate({
87
+ filename,
88
+ contentType,
89
+ size
90
+ });
91
+ if (validationResult !== true) {
92
+ return respond(
93
+ {
94
+ error: typeof validationResult === "string" ? validationResult : "Validation failed"
95
+ },
96
+ 400
97
+ );
98
+ }
99
+ }
100
+ const route = requestRoute || config.route;
101
+ const result = await config.stow.getPresignedUrl({
102
+ filename,
103
+ contentType,
104
+ size,
105
+ route
106
+ });
107
+ if ("dedupe" in result && result.dedupe) {
108
+ return respond(result, 200);
109
+ }
110
+ return respond(
111
+ {
112
+ ...result,
113
+ confirmUrl: getLocalConfirmPath(request.nextUrl.pathname)
114
+ },
115
+ 200
116
+ );
117
+ } catch (error) {
118
+ console.error("Presign error:", error);
119
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
120
+ const status = error.status;
121
+ return respond({ error: error.message }, status >= 400 ? status : 500);
122
+ }
123
+ return respond({ error: "Presign failed" }, 500);
124
+ }
125
+ }
126
+ return handler;
127
+ }
128
+ function createConfirmHandler(config) {
129
+ const corsEnabled = config.cors !== false;
130
+ const corsConfig = config.cors === false ? void 0 : config.cors;
131
+ const respond = (data, status) => {
132
+ const response = NextResponse.json(data, { status });
133
+ return corsEnabled ? withCors(response, corsConfig) : response;
134
+ };
135
+ async function handler(request) {
136
+ if (request.method === "OPTIONS") {
137
+ const response = new NextResponse(null, { status: 204 });
138
+ return corsEnabled ? withCors(response, corsConfig) : response;
139
+ }
140
+ try {
141
+ const body = await request.json();
142
+ const { fileKey, size, contentType } = body;
143
+ if (!fileKey || typeof fileKey !== "string") {
144
+ return respond({ error: "fileKey is required" }, 400);
145
+ }
146
+ if (typeof size !== "number" || size <= 0) {
147
+ return respond({ error: "size must be a positive number" }, 400);
148
+ }
149
+ if (!contentType || typeof contentType !== "string") {
150
+ return respond({ error: "contentType is required" }, 400);
151
+ }
152
+ if (!isValidMimeType(contentType)) {
153
+ return respond({ error: "Invalid contentType format" }, 400);
154
+ }
155
+ const result = await config.stow.confirmUpload({
156
+ fileKey,
157
+ size,
158
+ contentType
159
+ });
160
+ await config.onUploadComplete?.(result);
161
+ return respond(result, 200);
162
+ } catch (error) {
163
+ console.error("Confirm error:", error);
164
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
165
+ const status = error.status;
166
+ return respond({ error: error.message }, status >= 400 ? status : 500);
167
+ }
168
+ return respond({ error: "Confirm failed" }, 500);
169
+ }
170
+ }
171
+ return handler;
172
+ }
173
+ function createUploadHandler(config) {
174
+ let warned = false;
175
+ return async function handler(request) {
176
+ if (!warned) {
177
+ console.warn(
178
+ "[@howells/stow-next] createUploadHandler is deprecated. Use createPresignHandler + createConfirmHandler for direct uploads. Proxied uploads are limited by serverless payload limits (~4.5MB on Vercel)."
179
+ );
180
+ warned = true;
181
+ }
182
+ try {
183
+ const formData = await request.formData();
184
+ const file = formData.get("file");
185
+ const route = formData.get("route") || config.route;
186
+ if (!file) {
187
+ return NextResponse.json(
188
+ { error: "No file provided" },
189
+ { status: 400 }
190
+ );
191
+ }
192
+ if (config.maxSize && file.size > config.maxSize) {
193
+ return NextResponse.json(
194
+ {
195
+ error: `File too large. Maximum size is ${formatBytes(config.maxSize)}`
196
+ },
197
+ { status: 400 }
198
+ );
199
+ }
200
+ if (config.allowedTypes && config.allowedTypes.length > 0) {
201
+ const isAllowed = config.allowedTypes.some((type) => {
202
+ if (type.endsWith("/*")) {
203
+ const prefix = type.slice(0, -1);
204
+ return file.type.startsWith(prefix);
205
+ }
206
+ return file.type === type;
207
+ });
208
+ if (!isAllowed) {
209
+ return NextResponse.json(
210
+ { error: `File type "${file.type}" is not allowed` },
211
+ { status: 400 }
212
+ );
213
+ }
214
+ }
215
+ if (config.validate) {
216
+ const validationResult = await config.validate(file);
217
+ if (validationResult !== true) {
218
+ return NextResponse.json(
219
+ {
220
+ error: typeof validationResult === "string" ? validationResult : "File validation failed"
221
+ },
222
+ { status: 400 }
223
+ );
224
+ }
225
+ }
226
+ await config.onUploadBegin?.(file);
227
+ const result = await config.stow.uploadFile(file, {
228
+ filename: file.name,
229
+ contentType: file.type,
230
+ route
231
+ });
232
+ await config.onUploadComplete?.(result);
233
+ return NextResponse.json(result);
234
+ } catch (error) {
235
+ console.error("Upload error:", error);
236
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
237
+ const status = error.status;
238
+ return NextResponse.json(
239
+ { error: error.message },
240
+ { status: status >= 400 ? status : 500 }
241
+ );
242
+ }
243
+ return NextResponse.json({ error: "Upload failed" }, { status: 500 });
244
+ }
245
+ };
246
+ }
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
+ export {
261
+ createConfirmHandler,
262
+ createCorsPreflightHandler,
263
+ createPresignHandler,
264
+ createStowLoader,
265
+ createUploadHandler,
266
+ stowLoader
267
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@howells/stow-next",
3
+ "version": "0.1.0",
4
+ "description": "Next.js integration for Stow file storage",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/howells/stow.git",
9
+ "directory": "packages/stow-next"
10
+ },
11
+ "homepage": "https://stow.sh",
12
+ "keywords": [
13
+ "stow",
14
+ "file-storage",
15
+ "nextjs",
16
+ "image-loader",
17
+ "sdk"
18
+ ],
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.mjs",
26
+ "require": "./dist/index.js"
27
+ },
28
+ "./image-loader": {
29
+ "types": "./dist/image-loader.d.ts",
30
+ "import": "./dist/image-loader.mjs",
31
+ "require": "./dist/image-loader.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup src/index.ts src/image-loader.ts --format cjs,esm --dts --external next --external react",
39
+ "dev": "tsup src/index.ts src/image-loader.ts --format cjs,esm --dts --external next --external react --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest"
42
+ },
43
+ "peerDependencies": {
44
+ "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
45
+ "react": "^18.0.0 || ^19.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@stow/typescript-config": "workspace:*",
49
+ "@types/react": "19.2.14",
50
+ "next": "16.1.6",
51
+ "react": "^19.2.4",
52
+ "tsup": "^8.5.1",
53
+ "typescript": "^5.9.3",
54
+ "vitest": "^4.0.18"
55
+ }
56
+ }