formdata-io 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dougladmo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # 🚀 FormData IO
2
+
3
+ > TypeScript-first library for seamless FormData handling in frontend and backend.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/formdata-io.svg)](https://www.npmjs.com/package/formdata-io)
6
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/formdata-io)](https://bundlephobia.com/package/formdata-io)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ## Why?
10
+
11
+ Working with `multipart/form-data` in JavaScript is painful:
12
+
13
+ - ❌ Manual `formData.append()` calls everywhere
14
+ - ❌ Backend requires multer config, storage setup, and juggling `req.file` + `req.body`
15
+ - ❌ Poor TypeScript support
16
+ - ❌ Inconsistent APIs between frontend and backend
17
+
18
+ **FormData IO solves this:**
19
+
20
+ ```typescript
21
+ // Frontend: Write objects, not append calls
22
+ const formData = payload({ name: "João", avatar: file });
23
+
24
+ // Backend: Receive normalized payload, not scattered data
25
+ app.post('/upload', parser(), (req, res) => {
26
+ const { name, avatar } = req.payload; // ✨ Type-safe!
27
+ });
28
+ ```
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install formdata-io
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Frontend (React, Vue, Vanilla JS)
39
+
40
+ ```typescript
41
+ import { payload } from 'formdata-io/client';
42
+
43
+ const formData = payload({
44
+ name: "João Silva",
45
+ age: 25,
46
+ avatar: fileInput.files[0],
47
+ tags: ["admin", "user"],
48
+ metadata: { source: "web" }
49
+ });
50
+
51
+ // Use with fetch
52
+ fetch('/api/upload', {
53
+ method: 'POST',
54
+ body: formData
55
+ });
56
+
57
+ // Or with axios
58
+ axios.post('/api/upload', formData);
59
+ ```
60
+
61
+ ### Backend (Express)
62
+
63
+ ```typescript
64
+ import express from 'express';
65
+ import { parser } from 'formdata-io/server';
66
+
67
+ const app = express();
68
+
69
+ app.post('/api/upload', parser(), (req, res) => {
70
+ const { name, age, avatar, tags, metadata } = req.payload;
71
+
72
+ console.log(name); // "João Silva"
73
+ console.log(age); // 25 (auto-parsed as number)
74
+ console.log(avatar); // { buffer: Buffer, originalname: "...", ... }
75
+ console.log(tags); // ["admin", "user"]
76
+ console.log(metadata); // { source: "web" } (auto-parsed JSON)
77
+
78
+ res.json({ success: true });
79
+ });
80
+ ```
81
+
82
+ ## API Reference
83
+
84
+ ### Client API
85
+
86
+ #### `payload(data, options?)`
87
+
88
+ Converts a JavaScript object to FormData.
89
+
90
+ **Parameters:**
91
+ - `data: FormDataPayload` - Object to convert
92
+ - `options?: PayloadOptions` - Configuration options
93
+
94
+ **Returns:** `FormData`
95
+
96
+ **Options:**
97
+ ```typescript
98
+ {
99
+ indices: boolean; // Add indices to array fields (default: false)
100
+ nullsAsUndefineds: boolean; // Skip null values (default: false)
101
+ booleansAsIntegers: boolean; // Convert true/false to 1/0 (default: true)
102
+ }
103
+ ```
104
+
105
+ **Examples:**
106
+
107
+ ```typescript
108
+ // Basic usage
109
+ const formData = payload({
110
+ name: "User",
111
+ file: document.querySelector('input[type="file"]').files[0]
112
+ });
113
+
114
+ // With arrays
115
+ const formData = payload({
116
+ tags: ["admin", "user"] // → tags=admin&tags=user
117
+ });
118
+
119
+ // With indices
120
+ const formData = payload(
121
+ { tags: ["admin", "user"] },
122
+ { indices: true } // → tags[0]=admin&tags[1]=user
123
+ );
124
+
125
+ // Nested objects (auto-serialized as JSON)
126
+ const formData = payload({
127
+ metadata: { source: "web", version: 2 }
128
+ // → metadata='{"source":"web","version":2}'
129
+ });
130
+
131
+ // Date handling
132
+ const formData = payload({
133
+ createdAt: new Date() // → createdAt='2024-01-01T00:00:00.000Z'
134
+ });
135
+ ```
136
+
137
+ ### Server API
138
+
139
+ #### `parser(options?)`
140
+
141
+ Express middleware for parsing multipart/form-data.
142
+
143
+ **Parameters:**
144
+ - `options?: ParserOptions` - Configuration options
145
+
146
+ **Returns:** Express middleware function
147
+
148
+ **Options:**
149
+ ```typescript
150
+ {
151
+ maxFileSize: number; // Max file size in bytes (default: 10MB)
152
+ maxFiles: number; // Max number of files (default: 10)
153
+ autoParseJSON: boolean; // Auto-parse JSON strings (default: true)
154
+ autoParseNumbers: boolean; // Auto-convert numeric strings (default: true)
155
+ autoParseBooleans: boolean; // Auto-convert "true"/"false" (default: true)
156
+ }
157
+ ```
158
+
159
+ **Examples:**
160
+
161
+ ```typescript
162
+ // Default options (10MB, 10 files)
163
+ app.post('/upload', parser(), (req, res) => {
164
+ // req.payload contains all fields and files
165
+ });
166
+
167
+ // Custom file size limit
168
+ app.post('/photos', parser({ maxFileSize: 50 * 1024 * 1024 }), (req, res) => {
169
+ // Allow up to 50MB files
170
+ });
171
+
172
+ // Disable auto-parsing
173
+ app.post('/raw', parser({ autoParseJSON: false }), (req, res) => {
174
+ // All fields remain as strings
175
+ });
176
+ ```
177
+
178
+ #### `ParsedFile` Interface
179
+
180
+ ```typescript
181
+ interface ParsedFile {
182
+ fieldname: string; // Form field name
183
+ originalname: string; // Original filename
184
+ encoding: string; // File encoding
185
+ mimetype: string; // MIME type
186
+ size: number; // File size in bytes
187
+ buffer: Buffer; // File data
188
+ }
189
+ ```
190
+
191
+ ## TypeScript Support
192
+
193
+ Full TypeScript support with type inference:
194
+
195
+ ```typescript
196
+ import type { ParsedFile } from 'formdata-io/server';
197
+
198
+ app.post('/upload', parser(), (req, res) => {
199
+ const avatar = req.payload?.avatar as ParsedFile;
200
+
201
+ avatar.buffer; // Buffer
202
+ avatar.originalname; // string
203
+ avatar.mimetype; // string
204
+ avatar.size; // number
205
+ });
206
+ ```
207
+
208
+ ## Examples
209
+
210
+ See the [examples](./examples) directory for complete working examples.
211
+
212
+ **Run the example:**
213
+
214
+ ```bash
215
+ # Terminal 1: Start the server
216
+ npm run build
217
+ node examples/basic/server.ts
218
+
219
+ # Terminal 2: Open client.html in your browser
220
+ open examples/basic/client.html
221
+ ```
222
+
223
+ ## Comparison with Alternatives
224
+
225
+ | Feature | FormData IO | multer | busboy | object-to-formdata |
226
+ |---------|-------------|--------|--------|-------------------|
227
+ | Frontend + Backend | ✅ | ❌ | ❌ | ❌ |
228
+ | TypeScript-first | ✅ | ⚠️ | ⚠️ | ❌ |
229
+ | Zero config | ✅ | ❌ | ❌ | ✅ |
230
+ | Auto-parsing | ✅ | ❌ | ❌ | N/A |
231
+ | Bundle size | ~6KB | ~30KB | ~10KB | ~2KB |
232
+
233
+ ## How It Works
234
+
235
+ ### Client Side
236
+
237
+ The `payload()` function converts JavaScript objects to FormData by:
238
+
239
+ 1. **File/Blob detection**: Directly appends File and Blob objects
240
+ 2. **Array handling**: Supports both flat (`tags=a&tags=b`) and indexed (`tags[0]=a`) formats
241
+ 3. **Object serialization**: Nested objects are JSON-serialized
242
+ 4. **Type conversion**: Booleans, numbers, dates converted to strings
243
+
244
+ ### Server Side
245
+
246
+ The `parser()` middleware uses [busboy](https://github.com/mscdex/busboy) for stream-based parsing:
247
+
248
+ 1. **Stream processing**: Memory-efficient file handling
249
+ 2. **Size limits**: Enforced per-file and total limits
250
+ 3. **Auto-parsing**: Automatic type conversion (JSON, numbers, booleans)
251
+ 4. **Array normalization**: Multiple values with same key become arrays
252
+
253
+ ## Security
254
+
255
+ **Built-in protections:**
256
+ - ✅ File size limits (default: 10MB per file)
257
+ - ✅ File count limits (default: 10 files max)
258
+ - ✅ Stream-based processing (no memory exhaustion)
259
+ - ✅ Safe JSON parsing (fallback to string on error)
260
+
261
+ **Your responsibility:**
262
+ - ⚠️ File type validation (check `mimetype` and magic bytes)
263
+ - ⚠️ Filename sanitization (prevent path traversal)
264
+ - ⚠️ Virus scanning (if accepting user files)
265
+ - ⚠️ Storage security (S3 permissions, disk quotas)
266
+
267
+ ## License
268
+
269
+ MIT
270
+
271
+ ## Contributing
272
+
273
+ Contributions welcome! Please open an issue or PR.
274
+
275
+ ## Credits
276
+
277
+ Built with [busboy](https://github.com/mscdex/busboy) for multipart parsing.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Supported value types for FormData conversion
3
+ */
4
+ type FormDataValue = string | number | boolean | null | undefined | File | Blob | Date;
5
+ /**
6
+ * Recursive type for objects that can be converted to FormData
7
+ *
8
+ * Supports nested objects and arrays, with automatic serialization
9
+ */
10
+ type FormDataPayload = {
11
+ [key: string]: FormDataValue | FormDataValue[] | FormDataPayload | FormDataPayload[];
12
+ };
13
+ /**
14
+ * Configuration options for FormData conversion
15
+ */
16
+ interface PayloadOptions {
17
+ /**
18
+ * Add numeric indices to array field names
19
+ *
20
+ * @default false
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // indices: false → tags=a&tags=b
25
+ * // indices: true → tags[0]=a&tags[1]=b
26
+ * ```
27
+ */
28
+ indices?: boolean;
29
+ /**
30
+ * Skip null values instead of converting to empty strings
31
+ *
32
+ * @default false
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // nullsAsUndefineds: false → optional=""
37
+ * // nullsAsUndefineds: true → optional field not sent
38
+ * ```
39
+ */
40
+ nullsAsUndefineds?: boolean;
41
+ /**
42
+ * Convert boolean values to 1/0 instead of true/false
43
+ *
44
+ * @default true
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // booleansAsIntegers: true → active=1
49
+ * // booleansAsIntegers: false → active=true
50
+ * ```
51
+ */
52
+ booleansAsIntegers?: boolean;
53
+ }
54
+
55
+ /**
56
+ * Converts a JavaScript object to FormData
57
+ *
58
+ * @param data - Object to be converted
59
+ * @param options - Configuration options
60
+ * @returns FormData instance ready for submission
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const formData = payload({
65
+ * name: "João Silva",
66
+ * age: 25,
67
+ * avatar: fileInput.files[0],
68
+ * tags: ["admin", "user"],
69
+ * metadata: { source: "web" }
70
+ * });
71
+ *
72
+ * // Use with fetch
73
+ * fetch('/upload', { method: 'POST', body: formData });
74
+ *
75
+ * // Use with axios
76
+ * axios.post('/upload', formData);
77
+ * ```
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // With custom options
82
+ * const formData = payload(data, {
83
+ * indices: true, // tags[0]=admin&tags[1]=user
84
+ * booleansAsIntegers: false // active=true instead of active=1
85
+ * });
86
+ * ```
87
+ */
88
+ declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
89
+
90
+ export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Supported value types for FormData conversion
3
+ */
4
+ type FormDataValue = string | number | boolean | null | undefined | File | Blob | Date;
5
+ /**
6
+ * Recursive type for objects that can be converted to FormData
7
+ *
8
+ * Supports nested objects and arrays, with automatic serialization
9
+ */
10
+ type FormDataPayload = {
11
+ [key: string]: FormDataValue | FormDataValue[] | FormDataPayload | FormDataPayload[];
12
+ };
13
+ /**
14
+ * Configuration options for FormData conversion
15
+ */
16
+ interface PayloadOptions {
17
+ /**
18
+ * Add numeric indices to array field names
19
+ *
20
+ * @default false
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // indices: false → tags=a&tags=b
25
+ * // indices: true → tags[0]=a&tags[1]=b
26
+ * ```
27
+ */
28
+ indices?: boolean;
29
+ /**
30
+ * Skip null values instead of converting to empty strings
31
+ *
32
+ * @default false
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // nullsAsUndefineds: false → optional=""
37
+ * // nullsAsUndefineds: true → optional field not sent
38
+ * ```
39
+ */
40
+ nullsAsUndefineds?: boolean;
41
+ /**
42
+ * Convert boolean values to 1/0 instead of true/false
43
+ *
44
+ * @default true
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // booleansAsIntegers: true → active=1
49
+ * // booleansAsIntegers: false → active=true
50
+ * ```
51
+ */
52
+ booleansAsIntegers?: boolean;
53
+ }
54
+
55
+ /**
56
+ * Converts a JavaScript object to FormData
57
+ *
58
+ * @param data - Object to be converted
59
+ * @param options - Configuration options
60
+ * @returns FormData instance ready for submission
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const formData = payload({
65
+ * name: "João Silva",
66
+ * age: 25,
67
+ * avatar: fileInput.files[0],
68
+ * tags: ["admin", "user"],
69
+ * metadata: { source: "web" }
70
+ * });
71
+ *
72
+ * // Use with fetch
73
+ * fetch('/upload', { method: 'POST', body: formData });
74
+ *
75
+ * // Use with axios
76
+ * axios.post('/upload', formData);
77
+ * ```
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // With custom options
82
+ * const formData = payload(data, {
83
+ * indices: true, // tags[0]=admin&tags[1]=user
84
+ * booleansAsIntegers: false // active=true instead of active=1
85
+ * });
86
+ * ```
87
+ */
88
+ declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
89
+
90
+ export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // src/client/utils.ts
4
+ function isFileOrBlob(value) {
5
+ if (typeof File !== "undefined" && value instanceof File) return true;
6
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
7
+ return false;
8
+ }
9
+ function isPlainObject(value) {
10
+ if (typeof value !== "object" || value === null) return false;
11
+ if (isFileOrBlob(value)) return false;
12
+ if (value instanceof Date) return false;
13
+ if (Array.isArray(value)) return false;
14
+ return true;
15
+ }
16
+ function valueToString(value, options) {
17
+ if (value === null) {
18
+ return options.nullsAsUndefineds ? void 0 : "";
19
+ }
20
+ if (value === void 0) {
21
+ return void 0;
22
+ }
23
+ if (typeof value === "boolean") {
24
+ if (options.booleansAsIntegers) {
25
+ return value ? "1" : "0";
26
+ }
27
+ return value.toString();
28
+ }
29
+ if (value instanceof Date) {
30
+ return value.toISOString();
31
+ }
32
+ if (typeof value === "number") {
33
+ return value.toString();
34
+ }
35
+ if (typeof value === "string") {
36
+ return value;
37
+ }
38
+ return void 0;
39
+ }
40
+ function buildFieldName(parentKey, key, options) {
41
+ if (typeof key === "number") {
42
+ return options.indices ? `${parentKey}[${key}]` : parentKey;
43
+ }
44
+ return parentKey ? `${parentKey}[${key}]` : key;
45
+ }
46
+
47
+ // src/client/payload.ts
48
+ var DEFAULT_OPTIONS = {
49
+ indices: false,
50
+ nullsAsUndefineds: false,
51
+ booleansAsIntegers: true
52
+ };
53
+ function payload(data, options = {}) {
54
+ const opts = { ...DEFAULT_OPTIONS, ...options };
55
+ const formData = new FormData();
56
+ function append(key, value) {
57
+ if (isFileOrBlob(value)) {
58
+ formData.append(key, value);
59
+ return;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ value.forEach((item, index) => {
63
+ const fieldName = buildFieldName(key, index, opts);
64
+ append(fieldName, item);
65
+ });
66
+ return;
67
+ }
68
+ if (isPlainObject(value)) {
69
+ const jsonString = JSON.stringify(value);
70
+ formData.append(key, jsonString);
71
+ return;
72
+ }
73
+ const stringValue = valueToString(value, opts);
74
+ if (stringValue !== void 0) {
75
+ formData.append(key, stringValue);
76
+ }
77
+ }
78
+ Object.entries(data).forEach(([key, value]) => {
79
+ append(key, value);
80
+ });
81
+ return formData;
82
+ }
83
+
84
+ exports.payload = payload;
85
+ //# sourceMappingURL=index.js.map
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n"]}
@@ -0,0 +1,84 @@
1
+ // src/client/utils.ts
2
+ function isFileOrBlob(value) {
3
+ if (typeof File !== "undefined" && value instanceof File) return true;
4
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
5
+ return false;
6
+ }
7
+ function isPlainObject(value) {
8
+ if (typeof value !== "object" || value === null) return false;
9
+ if (isFileOrBlob(value)) return false;
10
+ if (value instanceof Date) return false;
11
+ if (Array.isArray(value)) return false;
12
+ return true;
13
+ }
14
+ function valueToString(value, options) {
15
+ if (value === null) {
16
+ return options.nullsAsUndefineds ? void 0 : "";
17
+ }
18
+ if (value === void 0) {
19
+ return void 0;
20
+ }
21
+ if (typeof value === "boolean") {
22
+ if (options.booleansAsIntegers) {
23
+ return value ? "1" : "0";
24
+ }
25
+ return value.toString();
26
+ }
27
+ if (value instanceof Date) {
28
+ return value.toISOString();
29
+ }
30
+ if (typeof value === "number") {
31
+ return value.toString();
32
+ }
33
+ if (typeof value === "string") {
34
+ return value;
35
+ }
36
+ return void 0;
37
+ }
38
+ function buildFieldName(parentKey, key, options) {
39
+ if (typeof key === "number") {
40
+ return options.indices ? `${parentKey}[${key}]` : parentKey;
41
+ }
42
+ return parentKey ? `${parentKey}[${key}]` : key;
43
+ }
44
+
45
+ // src/client/payload.ts
46
+ var DEFAULT_OPTIONS = {
47
+ indices: false,
48
+ nullsAsUndefineds: false,
49
+ booleansAsIntegers: true
50
+ };
51
+ function payload(data, options = {}) {
52
+ const opts = { ...DEFAULT_OPTIONS, ...options };
53
+ const formData = new FormData();
54
+ function append(key, value) {
55
+ if (isFileOrBlob(value)) {
56
+ formData.append(key, value);
57
+ return;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ value.forEach((item, index) => {
61
+ const fieldName = buildFieldName(key, index, opts);
62
+ append(fieldName, item);
63
+ });
64
+ return;
65
+ }
66
+ if (isPlainObject(value)) {
67
+ const jsonString = JSON.stringify(value);
68
+ formData.append(key, jsonString);
69
+ return;
70
+ }
71
+ const stringValue = valueToString(value, opts);
72
+ if (stringValue !== void 0) {
73
+ formData.append(key, stringValue);
74
+ }
75
+ }
76
+ Object.entries(data).forEach(([key, value]) => {
77
+ append(key, value);
78
+ });
79
+ return formData;
80
+ }
81
+
82
+ export { payload };
83
+ //# sourceMappingURL=index.mjs.map
84
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n"]}