@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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Stow Image Loader for Next.js
3
+ *
4
+ * Use Stow's image transformation service with Next.js Image component.
5
+ * Supports both vanity subdomain URLs ({slug}.stow.sh/{key}) and
6
+ * legacy path-based URLs (stow.sh/files/{bucketId}/{key}).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // next.config.js — global loader
11
+ * module.exports = {
12
+ * images: {
13
+ * loader: "custom",
14
+ * loaderFile: "./lib/stow-loader.ts",
15
+ * },
16
+ * };
17
+ *
18
+ * // lib/stow-loader.ts
19
+ * import { stowLoader } from "@howells/stow-next/image-loader";
20
+ * export default stowLoader;
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * // Per-component loader with crop options
26
+ * import { createStowLoader } from "@howells/stow-next/image-loader";
27
+ * import Image from "next/image";
28
+ *
29
+ * const avatarLoader = createStowLoader({
30
+ * fit: "cover",
31
+ * gravity: "face",
32
+ * aspectRatio: 1,
33
+ * defaultFormat: "webp",
34
+ * });
35
+ *
36
+ * function Avatar({ src }: { src: string }) {
37
+ * return <Image loader={avatarLoader} src={src} alt="Avatar" width={128} height={128} />;
38
+ * }
39
+ * ```
40
+ */
41
+ interface ImageLoaderProps {
42
+ quality?: number;
43
+ src: string;
44
+ width: number;
45
+ }
46
+ interface StowLoaderConfig {
47
+ /**
48
+ * Fixed aspect ratio (width / height).
49
+ * When set, height is calculated from the requested width.
50
+ * Example: 1 for square, 16/9 for widescreen, 4/5 for portrait.
51
+ */
52
+ aspectRatio?: number;
53
+ /**
54
+ * Base URL for the Stow service.
55
+ * @default "https://stow.sh"
56
+ */
57
+ baseUrl?: string;
58
+ /**
59
+ * Default output format.
60
+ */
61
+ defaultFormat?: "webp" | "avif" | "jpeg" | "png";
62
+ /**
63
+ * Default image quality (1-100).
64
+ * @default 75
65
+ */
66
+ defaultQuality?: number;
67
+ /**
68
+ * Resize fit mode (Cloudflare Images).
69
+ * - "scale-down" — shrink to fit, never enlarge
70
+ * - "contain" — fit within bounds preserving aspect ratio
71
+ * - "cover" — fill bounds, crop excess
72
+ * - "crop" — crop to exact dimensions
73
+ * - "pad" — fit within bounds, pad remaining space
74
+ */
75
+ fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
76
+ /**
77
+ * Crop gravity / anchor point.
78
+ * - "auto" — subject-aware ML cropping
79
+ * - "face" — face-detection cropping
80
+ * - directional: "left", "right", "top", "bottom", "center"
81
+ */
82
+ gravity?: "auto" | "face" | "left" | "right" | "top" | "bottom" | "center";
83
+ /**
84
+ * Bucket slug for proxying external images through Stow.
85
+ * When set, external URLs (http/https) are routed through proxy.stow.sh
86
+ * for R2 caching and on-the-fly image transforms.
87
+ */
88
+ proxySlug?: string;
89
+ }
90
+ /**
91
+ * Create a custom image loader with configuration.
92
+ *
93
+ * Returns a stable function reference safe for use as a Next.js `loader` prop.
94
+ */
95
+ declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality, }: ImageLoaderProps) => string;
96
+ /**
97
+ * Default Stow image loader.
98
+ *
99
+ * Handles vanity subdomain URLs ({slug}.stow.sh) and legacy
100
+ * path-based URLs (stow.sh/files/...). Appends width and quality
101
+ * as transform query params.
102
+ *
103
+ * ```js
104
+ * // next.config.js
105
+ * module.exports = {
106
+ * images: {
107
+ * loader: "custom",
108
+ * loaderFile: "./lib/stow-loader.ts",
109
+ * },
110
+ * };
111
+ *
112
+ * // lib/stow-loader.ts
113
+ * import { stowLoader } from "@howells/stow-next/image-loader";
114
+ * export default stowLoader;
115
+ * ```
116
+ */
117
+ declare const stowLoader: ({ src, width, quality, }: ImageLoaderProps) => string;
118
+
119
+ export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
@@ -0,0 +1,118 @@
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/image-loader.ts
21
+ var image_loader_exports = {};
22
+ __export(image_loader_exports, {
23
+ createStowLoader: () => createStowLoader,
24
+ stowLoader: () => stowLoader
25
+ });
26
+ module.exports = __toCommonJS(image_loader_exports);
27
+ var STOW_DOMAIN_PATTERN = /\.stow\.sh$/;
28
+ function isStowUrl(src) {
29
+ try {
30
+ const url = new URL(src);
31
+ return STOW_DOMAIN_PATTERN.test(url.hostname);
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function buildTransformParams(width, quality, config) {
37
+ const params = new URLSearchParams();
38
+ params.set("w", width.toString());
39
+ if (config.aspectRatio) {
40
+ params.set("h", Math.round(width / config.aspectRatio).toString());
41
+ }
42
+ params.set("q", quality.toString());
43
+ if (config.fit) {
44
+ params.set("fit", config.fit);
45
+ }
46
+ if (config.gravity) {
47
+ params.set("gravity", config.gravity);
48
+ }
49
+ if (config.defaultFormat) {
50
+ params.set("f", config.defaultFormat);
51
+ }
52
+ return params;
53
+ }
54
+ function transformStowUrl(src, baseUrl, params) {
55
+ const url = new URL(src, baseUrl);
56
+ const pathname = url.pathname;
57
+ if (pathname.startsWith("/files/")) {
58
+ const transformPath = pathname.replace("/files/", "/transform/");
59
+ return `${baseUrl}${transformPath}?${params.toString()}`;
60
+ }
61
+ if (pathname.startsWith("/transform/")) {
62
+ return `${baseUrl}${pathname}?${params.toString()}`;
63
+ }
64
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
65
+ }
66
+ function createStowLoader(config = {}) {
67
+ const {
68
+ baseUrl = "https://stow.sh",
69
+ defaultQuality = 75,
70
+ defaultFormat,
71
+ proxySlug,
72
+ fit,
73
+ gravity,
74
+ aspectRatio
75
+ } = config;
76
+ return function stowLoader2({
77
+ src,
78
+ width,
79
+ quality
80
+ }) {
81
+ const resolvedQuality = quality || defaultQuality;
82
+ const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
83
+ if (isStowUrl(src)) {
84
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
85
+ return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
86
+ }
87
+ if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
88
+ const params = buildTransformParams(width, resolvedQuality, paramConfig);
89
+ return transformStowUrl(src, baseUrl, params);
90
+ }
91
+ if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
92
+ const params = new URLSearchParams();
93
+ params.set("url", src);
94
+ params.set("w", width.toString());
95
+ if (aspectRatio) {
96
+ params.set("h", Math.round(width / aspectRatio).toString());
97
+ }
98
+ params.set("q", resolvedQuality.toString());
99
+ if (fit) {
100
+ params.set("fit", fit);
101
+ }
102
+ if (gravity) {
103
+ params.set("gravity", gravity);
104
+ }
105
+ if (defaultFormat) {
106
+ params.set("f", defaultFormat);
107
+ }
108
+ return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
109
+ }
110
+ return src;
111
+ };
112
+ }
113
+ var stowLoader = createStowLoader();
114
+ // Annotate the CommonJS export names for ESM import in node:
115
+ 0 && (module.exports = {
116
+ createStowLoader,
117
+ stowLoader
118
+ });
@@ -0,0 +1,8 @@
1
+ import {
2
+ createStowLoader,
3
+ stowLoader
4
+ } from "./chunk-WN3NWAEN.mjs";
5
+ export {
6
+ createStowLoader,
7
+ stowLoader
8
+ };
@@ -0,0 +1,156 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ export { StowLoaderConfig, createStowLoader, stowLoader } from './image-loader.mjs';
3
+
4
+ /**
5
+ * Stow Next.js Integration
6
+ *
7
+ * Provides Next.js-specific utilities for Stow file storage:
8
+ * - Route handlers for direct uploads (presign + confirm)
9
+ * - Image loader for next/image optimization
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // app/api/stow/presign/route.ts
14
+ * import { createPresignHandler } from "@howells/stow-next";
15
+ * import { StowServer } from "@howells/stow-server";
16
+ *
17
+ * const stow = new StowServer(process.env.STOW_API_KEY!);
18
+ * export const POST = createPresignHandler({ stow });
19
+ *
20
+ * // app/api/stow/confirm/route.ts
21
+ * import { createConfirmHandler } from "@howells/stow-next";
22
+ * export const POST = createConfirmHandler({ stow });
23
+ * ```
24
+ */
25
+
26
+ interface CorsConfig {
27
+ /** Allowed headers (default: ["Content-Type"]) */
28
+ allowedHeaders?: string[];
29
+ /** Max age for preflight cache in seconds (default: 86400 = 24 hours) */
30
+ maxAge?: number;
31
+ /** Allowed methods (default: ["POST", "OPTIONS"]) */
32
+ methods?: string[];
33
+ /** Allowed origins (default: "*") */
34
+ origin?: string | string[];
35
+ }
36
+ /**
37
+ * Create an OPTIONS handler for CORS preflight requests
38
+ */
39
+ declare function createCorsPreflightHandler(config?: CorsConfig): () => NextResponse;
40
+ interface StowServerLike {
41
+ confirmUpload: (request: {
42
+ fileKey: string;
43
+ size: number;
44
+ contentType: string;
45
+ }) => Promise<{
46
+ key: string;
47
+ url: string | null;
48
+ size: number;
49
+ contentType: string;
50
+ }>;
51
+ getPresignedUrl: (request: {
52
+ filename: string;
53
+ contentType: string;
54
+ size: number;
55
+ route?: string;
56
+ }) => Promise<{
57
+ dedupe: true;
58
+ key: string;
59
+ url: string | null;
60
+ size: number;
61
+ contentType: string;
62
+ } | {
63
+ fileKey: string;
64
+ uploadUrl: string;
65
+ r2Key: string;
66
+ confirmUrl: string;
67
+ dedupe?: false;
68
+ }>;
69
+ }
70
+ interface PresignHandlerConfig {
71
+ /** Allowed content types (supports wildcards like "image/*") */
72
+ allowedTypes?: string[];
73
+ /** CORS configuration (enabled by default for browser uploads) */
74
+ cors?: CorsConfig | false;
75
+ /** Maximum file size in bytes */
76
+ maxSize?: number;
77
+ /** Default route for organizing files */
78
+ route?: string;
79
+ /** Stow server instance */
80
+ stow: StowServerLike;
81
+ /** Custom validation function */
82
+ validate?: (request: {
83
+ filename: string;
84
+ contentType: string;
85
+ size: number;
86
+ }) => Promise<boolean | string> | boolean | string;
87
+ }
88
+ interface ConfirmHandlerConfig {
89
+ /** CORS configuration (enabled by default for browser uploads) */
90
+ cors?: CorsConfig | false;
91
+ /** Called after upload is confirmed */
92
+ onUploadComplete?: (result: {
93
+ key: string;
94
+ url: string | null;
95
+ size: number;
96
+ contentType: string;
97
+ }) => Promise<void> | void;
98
+ /** Stow server instance */
99
+ stow: StowServerLike;
100
+ }
101
+ /**
102
+ * Create a Next.js route handler for presigning uploads.
103
+ * This is called by @howells/stow-client to get a presigned URL for direct R2 upload.
104
+ *
105
+ * Returns both POST handler and OPTIONS handler for CORS preflight.
106
+ */
107
+ declare function createPresignHandler(config: PresignHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
108
+ /**
109
+ * Create a Next.js route handler for confirming uploads.
110
+ * This is called by @howells/stow-client after uploading to R2.
111
+ *
112
+ * Returns both POST handler and OPTIONS handler for CORS preflight.
113
+ */
114
+ declare function createConfirmHandler(config: ConfirmHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
115
+ interface StowLike {
116
+ uploadFile: (file: Blob, options?: {
117
+ filename?: string;
118
+ contentType?: string;
119
+ route?: string;
120
+ }) => Promise<{
121
+ key: string;
122
+ url: string | null;
123
+ size: number;
124
+ contentType: string;
125
+ }>;
126
+ }
127
+ interface UploadHandlerConfig {
128
+ /** Allowed content types (supports wildcards like "image/*") */
129
+ allowedTypes?: string[];
130
+ /** Maximum file size in bytes */
131
+ maxSize?: number;
132
+ /** Called before upload starts */
133
+ onUploadBegin?: (file: File) => Promise<void> | void;
134
+ /** Called after upload completes */
135
+ onUploadComplete?: (result: {
136
+ key: string;
137
+ url: string | null;
138
+ size: number;
139
+ contentType: string;
140
+ }) => Promise<void> | void;
141
+ /** Optional route for organizing files */
142
+ route?: string;
143
+ /** Stow server instance */
144
+ stow: StowLike;
145
+ /** Custom validation function */
146
+ validate?: (file: File) => Promise<boolean | string> | boolean | string;
147
+ }
148
+ /**
149
+ * Create a Next.js route handler for proxied file uploads.
150
+ *
151
+ * @deprecated Use createPresignHandler + createConfirmHandler for direct uploads.
152
+ * Proxied uploads are limited by serverless payload limits (~4.5MB on Vercel).
153
+ */
154
+ declare function createUploadHandler(config: UploadHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
155
+
156
+ export { type ConfirmHandlerConfig, type CorsConfig, type PresignHandlerConfig, type StowLike, type StowServerLike, type UploadHandlerConfig, createConfirmHandler, createCorsPreflightHandler, createPresignHandler, createUploadHandler };
@@ -0,0 +1,156 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ export { StowLoaderConfig, createStowLoader, stowLoader } from './image-loader.js';
3
+
4
+ /**
5
+ * Stow Next.js Integration
6
+ *
7
+ * Provides Next.js-specific utilities for Stow file storage:
8
+ * - Route handlers for direct uploads (presign + confirm)
9
+ * - Image loader for next/image optimization
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // app/api/stow/presign/route.ts
14
+ * import { createPresignHandler } from "@howells/stow-next";
15
+ * import { StowServer } from "@howells/stow-server";
16
+ *
17
+ * const stow = new StowServer(process.env.STOW_API_KEY!);
18
+ * export const POST = createPresignHandler({ stow });
19
+ *
20
+ * // app/api/stow/confirm/route.ts
21
+ * import { createConfirmHandler } from "@howells/stow-next";
22
+ * export const POST = createConfirmHandler({ stow });
23
+ * ```
24
+ */
25
+
26
+ interface CorsConfig {
27
+ /** Allowed headers (default: ["Content-Type"]) */
28
+ allowedHeaders?: string[];
29
+ /** Max age for preflight cache in seconds (default: 86400 = 24 hours) */
30
+ maxAge?: number;
31
+ /** Allowed methods (default: ["POST", "OPTIONS"]) */
32
+ methods?: string[];
33
+ /** Allowed origins (default: "*") */
34
+ origin?: string | string[];
35
+ }
36
+ /**
37
+ * Create an OPTIONS handler for CORS preflight requests
38
+ */
39
+ declare function createCorsPreflightHandler(config?: CorsConfig): () => NextResponse;
40
+ interface StowServerLike {
41
+ confirmUpload: (request: {
42
+ fileKey: string;
43
+ size: number;
44
+ contentType: string;
45
+ }) => Promise<{
46
+ key: string;
47
+ url: string | null;
48
+ size: number;
49
+ contentType: string;
50
+ }>;
51
+ getPresignedUrl: (request: {
52
+ filename: string;
53
+ contentType: string;
54
+ size: number;
55
+ route?: string;
56
+ }) => Promise<{
57
+ dedupe: true;
58
+ key: string;
59
+ url: string | null;
60
+ size: number;
61
+ contentType: string;
62
+ } | {
63
+ fileKey: string;
64
+ uploadUrl: string;
65
+ r2Key: string;
66
+ confirmUrl: string;
67
+ dedupe?: false;
68
+ }>;
69
+ }
70
+ interface PresignHandlerConfig {
71
+ /** Allowed content types (supports wildcards like "image/*") */
72
+ allowedTypes?: string[];
73
+ /** CORS configuration (enabled by default for browser uploads) */
74
+ cors?: CorsConfig | false;
75
+ /** Maximum file size in bytes */
76
+ maxSize?: number;
77
+ /** Default route for organizing files */
78
+ route?: string;
79
+ /** Stow server instance */
80
+ stow: StowServerLike;
81
+ /** Custom validation function */
82
+ validate?: (request: {
83
+ filename: string;
84
+ contentType: string;
85
+ size: number;
86
+ }) => Promise<boolean | string> | boolean | string;
87
+ }
88
+ interface ConfirmHandlerConfig {
89
+ /** CORS configuration (enabled by default for browser uploads) */
90
+ cors?: CorsConfig | false;
91
+ /** Called after upload is confirmed */
92
+ onUploadComplete?: (result: {
93
+ key: string;
94
+ url: string | null;
95
+ size: number;
96
+ contentType: string;
97
+ }) => Promise<void> | void;
98
+ /** Stow server instance */
99
+ stow: StowServerLike;
100
+ }
101
+ /**
102
+ * Create a Next.js route handler for presigning uploads.
103
+ * This is called by @howells/stow-client to get a presigned URL for direct R2 upload.
104
+ *
105
+ * Returns both POST handler and OPTIONS handler for CORS preflight.
106
+ */
107
+ declare function createPresignHandler(config: PresignHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
108
+ /**
109
+ * Create a Next.js route handler for confirming uploads.
110
+ * This is called by @howells/stow-client after uploading to R2.
111
+ *
112
+ * Returns both POST handler and OPTIONS handler for CORS preflight.
113
+ */
114
+ declare function createConfirmHandler(config: ConfirmHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
115
+ interface StowLike {
116
+ uploadFile: (file: Blob, options?: {
117
+ filename?: string;
118
+ contentType?: string;
119
+ route?: string;
120
+ }) => Promise<{
121
+ key: string;
122
+ url: string | null;
123
+ size: number;
124
+ contentType: string;
125
+ }>;
126
+ }
127
+ interface UploadHandlerConfig {
128
+ /** Allowed content types (supports wildcards like "image/*") */
129
+ allowedTypes?: string[];
130
+ /** Maximum file size in bytes */
131
+ maxSize?: number;
132
+ /** Called before upload starts */
133
+ onUploadBegin?: (file: File) => Promise<void> | void;
134
+ /** Called after upload completes */
135
+ onUploadComplete?: (result: {
136
+ key: string;
137
+ url: string | null;
138
+ size: number;
139
+ contentType: string;
140
+ }) => Promise<void> | void;
141
+ /** Optional route for organizing files */
142
+ route?: string;
143
+ /** Stow server instance */
144
+ stow: StowLike;
145
+ /** Custom validation function */
146
+ validate?: (file: File) => Promise<boolean | string> | boolean | string;
147
+ }
148
+ /**
149
+ * Create a Next.js route handler for proxied file uploads.
150
+ *
151
+ * @deprecated Use createPresignHandler + createConfirmHandler for direct uploads.
152
+ * Proxied uploads are limited by serverless payload limits (~4.5MB on Vercel).
153
+ */
154
+ declare function createUploadHandler(config: UploadHandlerConfig): (request: NextRequest) => Promise<NextResponse>;
155
+
156
+ export { type ConfirmHandlerConfig, type CorsConfig, type PresignHandlerConfig, type StowLike, type StowServerLike, type UploadHandlerConfig, createConfirmHandler, createCorsPreflightHandler, createPresignHandler, createUploadHandler };