parse-nested-form-data 1.0.0 → 1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"parse-nested-form-data.umd.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @name parse-nested-form-data\n * @license MIT license.\n * @copyright (c) 2022 Christian Schurr\n * @author Christian Schurr <chris@schurr.dev>\n */\n\nimport {File} from '@remix-run/web-file'\n\n/**\n * Thrown when a path is used multiple times or has missmatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.key = key\n }\n}\n\ntype JsonObject = {[Key in string]?: JsonValue}\ntype JsonArray = Array<JsonValue>\ntype JsonValue =\n | string\n | number\n | JsonObject\n | JsonArray\n | boolean\n | null\n | File\ntype JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !(val instanceof File)\n )\n}\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nfunction defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\ntype DefaultTransform = typeof defaultTransform\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: ([path, value], defaultTransform) => {\n * return {\n * path,\n * value:\n * typeof value === 'string'\n * ? value.toUpperCase()\n * : defaultTransform(value),\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key)\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\ntype PathPart = {\n path: string\n type: 'object' | 'array'\n default: {} | []\n pathToPart: string\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @returns {Array<PathPart>} the extracted path parts\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nfunction extractPathParts(path: string): Array<PathPart> {\n const re = /((?<array>\\d*)\\]|(?<pathPart>[^.[]+))(?<nextType>\\[|\\.|$)/g\n\n return Array.from(path.matchAll(re)).map<PathPart>(match => {\n // self casted RegexExpMatchArray to custom\n const typedMatch = match as unknown as [string, string] &\n RegExpExecArray & {\n groups:\n | {\n pathPart: undefined\n array: string\n nextType: '[' | '.' | ''\n }\n | {\n pathPart: string\n array: undefined\n nextType: '[' | '.' | ''\n }\n }\n const {array, pathPart, nextType} = typedMatch.groups\n const type = array === undefined ? 'object' : 'array'\n const nextDefault = nextType === '[' ? [] : {}\n return {\n path: array ?? pathPart,\n type,\n default: nextDefault,\n pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length),\n }\n })\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`\n * and a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`.\n *\n * @param pathPart - the path part to get the setter and getter for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the setter and getter for the path part\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n] {\n if (pathPart.type === 'object') {\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n return [\n currentObject[pathPart.path],\n val => (currentObject[pathPart.path] = val),\n ]\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n const isOrdered = pathPart.path !== ''\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val)]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData, defaultTransform)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})\n * // => {a: ['foobar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := `^(\\+|\\-|\\&)?([^\\.]+?(\\[\\d*\\])*)(\\.[^\\.]+?(\\[\\d*\\])*)*$` (e.g. `+a[][1].b`)\n * - `value` := `string` or `File`\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * @returns {JsonObject} the parsed JSON object\n * @throws `DuplicateKeyError` if\n * - a path part is an object and the path part is already defined as an object\n * - a path part is an array and the path part is already defined as an array\n * @throws `MixedArrayError` if at a specific path part an unordered array is \n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n }: ParseFormDataOptions = {},\n): JsonObject {\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path)\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n }\n })\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // replace array with a squashed array\n // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])\n orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0))\n }\n\n return result\n}\n"],"names":["DuplicateKeyError","Error","constructor","key","MixedArrayError","isJsonObject","val","Array","isArray","File","defaultTransform","entry","path","value","startsWith","slice","Number","Boolean","extractPathParts","re","from","matchAll","map","match","typedMatch","array","pathPart","nextType","groups","type","undefined","nextDefault","default","pathToPart","index","length","handlePathPart","currentPathObject","arraysWithOrder","currentObject","currentArray","isOrdered","isOrderedArray","has","add","order","parseFormData","formData","removeEmptyString","transformEntry","result","Set","pathParts","forEach","idx","nextPathValue","setNextPathValue","nextPathObject","orderedArray","splice","flat"],"mappings":";;;;;;EAAA;EACA;EACA;EACA;EACA;EACA;;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,MAAMA,iBAAiB,SAASC,KAAK,CAAC;IAE3CC,WAAW,CAACC,GAAW,EAAE;EACvB,IAAA,KAAK,CAAE,CAAA,2BAAA,EAA6BA,GAAI,CAAA,CAAC,CAAC,CAAA;EAAA,IAAA,IAAA,CAF5CA,GAAG,GAAA,KAAA,CAAA,CAAA;MAGD,IAAI,CAACA,GAAG,GAAGA,GAAG,CAAA;EAChB,GAAA;EACF,CAAA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,MAAMC,eAAe,SAASH,KAAK,CAAC;IAEzCC,WAAW,CAACC,GAAW,EAAE;EACvB,IAAA,KAAK,CAAE,CAAA,yBAAA,EAA2BA,GAAI,CAAA,CAAC,CAAC,CAAA;EAAA,IAAA,IAAA,CAF1CA,GAAG,GAAA,KAAA,CAAA,CAAA;MAGD,IAAI,CAACA,GAAG,GAAGA,GAAG,CAAA;EAChB,GAAA;EACF,CAAA;EAcA,SAASE,YAAY,CAACC,GAAc,EAAqB;IACvD,OACE,OAAOA,GAAG,KAAK,QAAQ,IACvB,CAACC,KAAK,CAACC,OAAO,CAACF,GAAG,CAAC,IACnBA,GAAG,KAAK,IAAI,IACZ,EAAEA,GAAG,YAAYG,YAAI,CAAC,CAAA;EAE1B,CAAA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAASC,gBAAgB,CAACC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,CAACH,YAAY,CAAC,CAAA;EACxD,EAAA,IAAIC,SAAS,EAAE;EACbH,IAAAA,eAAe,CAACM,GAAG,CAACJ,YAAY,CAAC,CAAA;EACnC,GAAA;EACA,EAAA,IACG,CAACC,SAAS,IAAIC,cAAc,IAC5BD,SAAS,IAAI,CAACC,cAAc,IAAIF,YAAY,CAACL,MAAM,GAAG,CAAE,EACzD;EACA,IAAA,MAAM,IAAI/B,eAAe,CAACsB,QAAQ,CAACO,UAAU,CAAC,CAAA;EAChD,GAAA;EAEA,EAAA,MAAMY,KAAK,GAAGJ,SAAS,GAAGzB,MAAM,CAACU,QAAQ,CAACd,IAAI,CAAC,GAAG4B,YAAY,CAACL,MAAM,CAAA;EACrE,EAAA,OAAO,CAACK,YAAY,CAACK,KAAK,CAAC,EAAEvC,GAAG,IAAKkC,YAAY,CAACK,KAAK,CAAC,GAAGvC,GAAI,CAAC,CAAA;EAClE,CAAA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,SAASwC,aAAa,CAC3BC,QAA2C,EAK/B,KAAA,EAAA;IAAA,IAJZ;EACEC,IAAAA,iBAAiB,GAAG,KAAK;EACzBC,IAAAA,cAAc,GAAGvC,gBAAAA;KACI,GAAA,KAAA,KAAA,KAAA,CAAA,GAAG,EAAE,GAAA,KAAA,CAAA;IAE5B,MAAMwC,MAAkB,GAAG,EAAE,CAAA;;EAE7B;EACA,EAAA,MAAMZ,eAAsC,GAAG,IAAIa,GAAG,EAAE,CAAA;IAExD,KAAK,MAAMxC,KAAK,IAAIJ,KAAK,CAACa,IAAI,CAAC2B,QAAQ,CAAC,EAAE;MACxC,IAAIC,iBAAiB,IAAIrC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,SAAA;MAE1C,MAAM;QAACC,IAAI;EAAEC,MAAAA,KAAAA;EAAK,KAAC,GAAGoC,cAAc,CAACtC,KAAK,EAAED,gBAAgB,CAAC,CAAA;EAC7D,IAAA,MAAM0C,SAAS,GAAGlC,gBAAgB,CAACN,IAAI,CAAC,CAAA;MAExC,IAAIyB,iBAAyC,GAAGa,MAAM,CAAA;EACtDE,IAAAA,SAAS,CAACC,OAAO,CAAC,CAAC3B,QAAQ,EAAE4B,GAAG,KAAK;EACnC,MAAA,MAAM,CAACC,aAAa,EAAEC,gBAAgB,CAAC,GAAGpB,cAAc,CACtDV,QAAQ,EACRW,iBAAiB,EACjBC,eAAe,CAChB,CAAA;EAED,MAAA,IAAIc,SAAS,CAACjB,MAAM,GAAG,CAAC,KAAKmB,GAAG,EAAE;UAChC,IAAIC,aAAa,KAAKzB,SAAS,EAAE;EAC/B,UAAA,MAAM,IAAI9B,iBAAiB,CAAC0B,QAAQ,CAACO,UAAU,CAAC,CAAA;EAClD,SAAA;UACAuB,gBAAgB,CAAC3C,KAAK,CAAC,CAAA;EACzB,OAAC,MAAM;EACL,QAAA,IACE0C,aAAa,KAAKzB,SAAS,IAC3B,CAACzB,YAAY,CAACkD,aAAa,CAAC,IAC5B,CAAChD,KAAK,CAACC,OAAO,CAAC+C,aAAa,CAAC,EAC7B;EACA,UAAA,MAAM,IAAIvD,iBAAiB,CAAC0B,QAAQ,CAACO,UAAU,CAAC,CAAA;EAClD,SAAA;EAEA,QAAA,MAAMwB,cAAc,GAAGF,aAAa,IAAI7B,QAAQ,CAACM,OAAO,CAAA;EACxDK,QAAAA,iBAAiB,GAAGoB,cAAc,CAAA;UAClCD,gBAAgB,CAACC,cAAc,CAAC,CAAA;EAClC,OAAA;EACF,KAAC,CAAC,CAAA;EACJ,GAAA;IAEA,KAAK,MAAMC,YAAY,IAAInD,KAAK,CAACa,IAAI,CAACkB,eAAe,CAAC,EAAE;EACtD;EACA;EACAoB,IAAAA,YAAY,CAACC,MAAM,CAAC,CAAC,EAAED,YAAY,CAACvB,MAAM,EAAE,GAAGuB,YAAY,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;EACtE,GAAA;EAEA,EAAA,OAAOV,MAAM,CAAA;EACf;;;;;;;;;;;;"}
1
+ {"version":3,"file":"parse-nested-form-data.umd.js","names":[],"sources":["../src/errors.ts","../src/path-parts.ts","../src/transform.ts","../src/index.ts"],"sourcesContent":["// The errors `parseFormData` throws. All of them are exported from the package.\n// Each class sets `name` as a string literal: minifiers rename the classes, so\n// `this.constructor.name` would be `e` or `t` in the minified build.\n\n/**\n * Thrown when a path is used multiple times or has mismatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.name = 'DuplicateKeyError'\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.name = 'MixedArrayError'\n this.key = key\n }\n}\n/**\n * Cuts a path to 100 characters plus `...`, so a huge path from a client\n * doesn't end up in full in logs or error responses.\n */\nfunction shortKey(key: string): string {\n return key.length > 100 ? `${key.slice(0, 100)}...` : key\n}\n\n/**\n * Thrown when an object path part is `__proto__`, `constructor` or\n * `prototype`, which could modify `Object.prototype`. The names are rejected\n * at any position in the path. Entries with other names on `Object.prototype`\n * (`toString`, ...) are removed instead (see `parseFormData`).\n * `key` is the path up to the forbidden part, cut to 100 characters plus `...`\n * when it's longer.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('__proto__.polluted', 'yes')\n * parseFormData(formData)\n * // throws ForbiddenKeyError('__proto__')\n * ```\n */\nexport class ForbiddenKeyError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Forbidden key at path part ${key}`)\n this.name = 'ForbiddenKeyError'\n this.key = key\n }\n}\n/**\n * Thrown when a path has more path parts than the `depth` option allows.\n * `key` is the path up to the first path part over the limit, cut to\n * 100 characters plus `...` when it's longer.\n * Within one entry the depth is checked first, so a path that's too deep throws\n * `DepthLimitError` even if it would also throw another error, like a\n * `ForbiddenKeyError` for `__proto__`.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b[0].c', 'x')\n * parseFormData(formData, {depth: 2})\n * // throws DepthLimitError('a.b[0]')\n * ```\n */\nexport class DepthLimitError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Depth limit exceeded at path part ${key}`)\n this.name = 'DepthLimitError'\n this.key = key\n }\n}\n/**\n * Thrown when an array index in a path is larger than `4294967294`, the\n * largest index a JavaScript array can hold.\n * `key` is the path up to that array part, cut to 100 characters plus `...`\n * when it's longer.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[4294967295]', 'b')\n * parseFormData(formData)\n * // throws InvalidPathError('a[4294967295]')\n * ```\n */\nexport class InvalidPathError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Invalid path at path part ${key}`)\n this.name = 'InvalidPathError'\n this.key = key\n }\n}\n","// Splits a FormData path into its path parts.\n\nimport {DepthLimitError} from './errors'\nimport type {JsonArray, JsonObject} from './types'\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key or [key])\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\nexport type PathPart = {\n path: string\n type: 'object' | 'array'\n default: JsonObject | JsonArray\n pathToPart: string\n}\n/**\n * Finds the `]` that ends the bracket part starting at the `[` at `open`.\n * Returns -1 if this `[` is not a bracket part but a normal character of the key.\n *\n * Brackets can be nested: in `a[b[c]]` the last `]` belongs to\n * the first `[`.\n *\n * Special cases:\n * - Some `[` are never closed. The part then ends at the last `]`:\n * `a[[0]` => key `[0`.\n * - If there is no `]` after the `[` at all, the `[` is a normal character:\n * `a[0` => key `a[0`.\n * - The same goes for a `]` without a `[`: `a]b` => key `a]b`, and\n * `a[b]]` => key `]` inside `b`.\n *\n * The scan stops at the returned `]`. The caller continues from there, so no\n * character of the path is scanned twice.\n */\nfunction closeOf(path: string, open: number, lastClose: number): number {\n if (open > lastClose) return -1\n let nesting = 0\n for (let idx = open; idx < lastClose; idx++) {\n if (path[idx] === '[') nesting++\n else if (path[idx] === ']' && --nesting === 0) return idx\n }\n return lastClose\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @param depth - the maximum number of path parts (default: `Infinity`)\n * @returns {Array<PathPart>} the extracted path parts\n * @throws `DepthLimitError` if the path has more path parts than `depth`, at\n * the first part over the limit\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[b][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: [], pathToPart: 'a[b]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[b][0]'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nexport function extractPathParts(\n path: string,\n depth: number = Infinity,\n): Array<PathPart> {\n const lastClose = path.lastIndexOf(']')\n\n const parts: Array<PathPart> = []\n const addPart = (key: string, type: PathPart['type'], end: number): void => {\n // Empty keys come from `a..b`, `a.`, `.a` or a `.` or `[` after `]`.\n if (key === '' && type === 'object') return\n if (parts.length === depth) throw new DepthLimitError(path.slice(0, end))\n parts.push({path: key, type, default: {}, pathToPart: path.slice(0, end)})\n }\n\n let start = 0\n for (let idx = 0; idx < path.length; idx++) {\n const close = path[idx] === '[' ? closeOf(path, idx, lastClose) : -1\n if (path[idx] !== '.' && close < idx) continue\n\n addPart(path.slice(start, idx), 'object', idx)\n if (close > idx) {\n // Digits or nothing is an array index, anything else is the key as it\n // is (`a[b.c]` => key `b.c`).\n const key = path.slice(idx + 1, close)\n addPart(key, /^\\d*$/.test(key) ? 'array' : 'object', close + 1)\n idx = close\n }\n start = idx + 1\n }\n addPart(path.slice(start), 'object', path.length)\n\n // A part holds an array if the next part is an array index.\n parts.forEach((part, idx) => {\n if (parts[idx + 1]?.type === 'array') part.default = []\n })\n return parts\n}\n","// Turns a FormData entry into a path and a value before it is parsed.\n\nimport type {JsonLeafValue} from './types'\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nexport function defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\nexport type DefaultTransform = typeof defaultTransform\n","import {\n DepthLimitError,\n DuplicateKeyError,\n ForbiddenKeyError,\n InvalidPathError,\n MixedArrayError,\n} from './errors'\nimport {extractPathParts, type PathPart} from './path-parts'\nimport {defaultTransform, type DefaultTransform} from './transform'\nimport type {JsonArray, JsonLeafValue, JsonObject, JsonValue} from './types'\n\nexport {\n DepthLimitError,\n DuplicateKeyError,\n ForbiddenKeyError,\n InvalidPathError,\n MixedArrayError,\n}\n\n// Rejected even when a runtime doesn't define them on `Object.prototype`\n// (e.g. `node --disable-proto=delete`).\nconst FORBIDDEN_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n// The largest valid array index (2 ** 32 - 2).\nconst MAX_ARRAY_INDEX = 4_294_967_294\n\n// Any property on `Object.prototype` at parse time, including ones added by a\n// newer runtime or by other code.\nfunction isPrototypeKey(key: string): boolean {\n return Object.getOwnPropertyDescriptor(Object.prototype, key) !== undefined\n}\n\n/**\n * Detects `File` and `Blob` values by their `Symbol.toStringTag`, which works\n * for native instances, polyfills (e.g. `@remix-run/web-file`) and values from\n * other realms, where `instanceof` would fail.\n */\nfunction isFileLike(val: unknown): boolean {\n const tag = Object.prototype.toString.call(val)\n return tag === '[object File]' || tag === '[object Blob]'\n}\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !isFileLike(val)\n )\n}\n\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n * @param depth - the maximum number of path parts in a path, counting every\n * part (`a.b[0]` has 3); a positive integer or `Infinity` (default: `Infinity`).\n * For untrusted input, `32` is recommended; 2.0 will lower the default.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'x')\n * parseFormData(formData, {depth: 2})\n * // => {a: {b: 'x'}}\n * formData.append('a.c.d', 'y')\n * parseFormData(formData, {depth: 2})\n * // throws DepthLimitError('a.c.d')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: (entry, defaultTransform) => {\n * const {path, value} = defaultTransform(entry)\n * return {\n * path,\n * value: typeof value === 'string' ? value.toUpperCase() : value,\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n depth?: number\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`,\n * a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`, and the key (object key or array index) it uses.\n *\n * @param pathPart - the path part to get the value, setter and key for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the current value, a setter for it, and the key (object key or\n * array index) in `currentPathObject`\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}, key = 'a'\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject.a, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}, key = 0\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n key: string | number,\n] {\n if (pathPart.type === 'object') {\n const key = pathPart.path\n if (FORBIDDEN_OBJECT_KEYS.has(key)) {\n throw new ForbiddenKeyError(pathPart.pathToPart)\n }\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n // Only own properties are existing values, so an inherited `toString`\n // isn't read as one.\n const value = Object.prototype.hasOwnProperty.call(currentObject, key)\n ? currentObject[key]\n : undefined\n return [\n value,\n val => {\n if (!isPrototypeKey(key)) {\n currentObject[key] = val\n return\n }\n // We use `Object.defineProperty` so that read-only properties and\n // setters on the prototype are not used, but shadowed by a property\n // on our object (the prototype stays untouched). The entry is removed\n // at the end (see `parseFormData`).\n Object.defineProperty(currentObject, key, {\n value: val,\n writable: true,\n enumerable: true,\n configurable: true,\n })\n },\n key,\n ]\n }\n const isOrdered = pathPart.path !== ''\n // `path` only contains digits here, so `Number` never returns NaN. Indices\n // too large to be safe integers are above the limit as well.\n if (isOrdered && Number(pathPart.path) > MAX_ARRAY_INDEX) {\n throw new InvalidPathError(pathPart.pathToPart)\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val), order]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[key]` after path part => nest into `objects`, same as `.key`; the text\n * in brackets is the key as it is (`a[b.c]` => key `b.c`)\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[b]', 'foo')\n * formData.append('a.c', 'bar')\n * parseFormData(formData)\n * // => {a: {b: 'foo', c: 'bar'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {\n * transformEntry: ([path, value]) => ({\n * path,\n * value: typeof value === 'string' ? value + 'bar' : value,\n * }),\n * })\n * // => {a: ['foobar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := an optional prefix (`+`, `-`, `&`), then path parts: keys after\n * `.` or in brackets (`[key]`), and array parts `[]` or `[\\d+]` (e.g.\n * `+a[][1].b[c]`)\n * - `value` := `string` or `File` (native or polyfilled `File` values are kept\n * as they are)\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * - `depth` - the maximum number of path parts in a path, counting every part\n * (default: `Infinity`)\n * @returns {JsonObject} the parsed JSON object\n * @throws `TypeError` if `depth` isn't a positive integer or `Infinity`\n * @throws `DepthLimitError` if a path has more path parts than `depth`\n * - e.g. `a.b[0]` with `depth: 2` => `DepthLimitError('a.b[0]')`\n * @throws `DuplicateKeyError` if\n * - two entries set a value at the same path\n * - e.g. `a` and `a`, `a[0]` and `a[0]`, `a.b` and `a[b]`\n * - an entry uses a path part as an object, array or value and another entry\n * uses it differently\n * - e.g. `a` and `a.b`, `a.b` and `a[]`, `a[]` and `a.b`\n * - the path starts with an array part\n * - e.g. `[0]` or `[]`\n * @throws `MixedArrayError` if at a specific path part an unordered array is\n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n * @throws `ForbiddenKeyError` if an object path part is `__proto__`,\n * `constructor` or `prototype`\n * - e.g. `__proto__.polluted`\n * @throws `InvalidPathError` if an array index is larger than `4294967294`\n * - e.g. `a[4294967295]`\n *\n * Every error's `key` is the path after `transformEntry`, not the field name\n * (e.g. `+a.b.c` with `depth: 2` => `DepthLimitError('a.b.c')`).\n *\n * An entry with any other object path part named like a property on\n * `Object.prototype` (`toString`, `hasOwnProperty`, ...) is removed from the\n * result. It's parsed like any other entry first, so it causes the same errors\n * as an unused name: an error doesn't tell the client which properties exist\n * on the server's `Object.prototype`.\n * - e.g. `user.toString` => `{}`\n * - e.g. `user[toString]` => `{}`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n depth = Infinity,\n }: ParseFormDataOptions = {},\n): JsonObject {\n if (depth !== Infinity && !(Number.isInteger(depth) && depth > 0)) {\n throw new TypeError(\n `depth must be a positive integer or Infinity, got ${String(depth)}`,\n )\n }\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n // What the removed entries created, in order. `container` is the object or\n // array an entry created on its way to its value; it's undefined for the\n // value itself.\n const createdByRemoved: Array<\n [\n parent: JsonObject | JsonArray,\n key: string | number,\n container: JsonObject | JsonArray | undefined,\n ]\n > = []\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path, depth)\n // Removed entries are still built like any other entry, so they cause the\n // same errors as an unused name, and undone below.\n const isRemoved = pathParts.some(\n pathPart => pathPart.type === 'object' && isPrototypeKey(pathPart.path),\n )\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue, key] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n const parent = currentPathObject\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n if (isRemoved) createdByRemoved.push([parent, key, undefined])\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n if (isRemoved && nextPathValue === undefined) {\n createdByRemoved.push([parent, key, nextPathObject])\n }\n }\n })\n }\n\n // Delete what the removed entries added. Go backwards so children are\n // deleted before their parents. A container that still has keys is also\n // used by a kept entry, so it stays.\n for (let idx = createdByRemoved.length - 1; idx >= 0; idx--) {\n const [parent, key, container] = createdByRemoved[idx]!\n if (container && Object.keys(container).length > 0) continue\n Reflect.deleteProperty(parent, key)\n // The hole is squashed below like the gaps in ordered arrays.\n if (Array.isArray(parent)) arraysWithOrder.add(parent)\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // Squash the sparse array in place (e.g. [0, , 1] => [0, 1]).\n // `Object.values` only visits slots that are set, in ascending index\n // order, so the cost depends on the number of entries, not on `length`.\n const values = Object.values(orderedArray)\n orderedArray.length = 0\n for (const value of values) orderedArray.push(value)\n }\n\n return result\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- !function(t,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@remix-run/web-file")):"function"==typeof define&&define.amd?define(["exports","@remix-run/web-file"],r):r((t="undefined"!=typeof globalThis?globalThis:t||self).parseNestedFormData={},t.RemixRunWebFile)}(this,(function(t,r){"use strict";
2
1
  /**
3
- * @name parse-nested-form-data
4
- * @license MIT license.
5
- * @copyright (c) 2022 Christian Schurr
6
- * @author Christian Schurr <chris@schurr.dev>
7
- */class e extends Error{constructor(t){super(`Duplicate key at path part ${t}`),this.key=void 0,this.key=t}}class a extends Error{constructor(t){super(`Mixed array at path part ${t}`),this.key=void 0,this.key=t}}function o(t){let r=t[0],e=t[1];return r.startsWith("+")?(r=r.slice(1),e=Number(e)):r.startsWith("&")?(r=r.slice(1),e="on"===e||"true"===e||Boolean(Number(e))):r.startsWith("-")&&(r=r.slice(1),e=null),{path:r,value:e}}function n(t){return Array.from(t.matchAll(/((?<array>\d*)\]|(?<pathPart>[^.[]+))(?<nextType>\[|\.|$)/g)).map((r=>{const e=r,{array:a,pathPart:o,nextType:n}=e.groups;return{path:a??o,type:void 0===a?"object":"array",default:"["===n?[]:{},pathToPart:t.slice(0,e.index+e[1].length)}}))}function i(t,r,o){if("object"===t.type){if(Array.isArray(r))throw new e(t.pathToPart);const a=r;return[a[t.path],r=>a[t.path]=r]}if(!Array.isArray(r))throw new e(t.pathToPart);const n=r,i=""!==t.path,s=o.has(n);if(i&&o.add(n),!i&&s||i&&!s&&n.length>0)throw new a(t.pathToPart);const f=i?Number(t.path):n.length;return[n[f],t=>n[f]=t]}t.DuplicateKeyError=e,t.MixedArrayError=a,t.parseFormData=function(t,a){let{removeEmptyString:s=!1,transformEntry:f=o}=void 0===a?{}:a;const l={},p=new Set;for(const a of Array.from(t)){if(s&&""===a[1])continue;const{path:t,value:u}=f(a,o),c=n(t);let h=l;c.forEach(((t,a)=>{const[o,n]=i(t,h,p);if(c.length-1===a){if(void 0!==o)throw new e(t.pathToPart);n(u)}else{if(void 0!==o&&("object"!=typeof(s=o)||Array.isArray(s)||null===s||s instanceof r.File)&&!Array.isArray(o))throw new e(t.pathToPart);const a=o??t.default;h=a,n(a)}var s}))}for(const t of Array.from(p))t.splice(0,t.length,...t.flat(0));return l},Object.defineProperty(t,"__esModule",{value:!0})}));
8
- //# sourceMappingURL=parse-nested-form-data.umd.min.js.map
2
+ * @name parse-nested-form-data
3
+ * @license MIT license.
4
+ * @copyright (c) 2022 Christian Schurr
5
+ * @author Christian Schurr <chris@schurr.dev>
6
+ */
7
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.parseNestedFormData={}))})(this,function(e){Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var t=class extends Error{constructor(e){super(`Duplicate key at path part ${e}`),this.name=`DuplicateKeyError`,this.key=e}},n=class extends Error{constructor(e){super(`Mixed array at path part ${e}`),this.name=`MixedArrayError`,this.key=e}};function r(e){return e.length>100?`${e.slice(0,100)}...`:e}var i=class extends Error{constructor(e){e=r(e),super(`Forbidden key at path part ${e}`),this.name=`ForbiddenKeyError`,this.key=e}},a=class extends Error{constructor(e){e=r(e),super(`Depth limit exceeded at path part ${e}`),this.name=`DepthLimitError`,this.key=e}},o=class extends Error{constructor(e){e=r(e),super(`Invalid path at path part ${e}`),this.name=`InvalidPathError`,this.key=e}};function s(e,t,n){if(t>n)return-1;let r=0;for(let i=t;i<n;i++)if(e[i]===`[`)r++;else if(e[i]===`]`&&--r===0)return i;return n}function c(e,t=1/0){let n=e.lastIndexOf(`]`),r=[],i=(n,i,o)=>{if(n!==``||i!==`object`){if(r.length===t)throw new a(e.slice(0,o));r.push({path:n,type:i,default:{},pathToPart:e.slice(0,o)})}},o=0;for(let t=0;t<e.length;t++){let r=e[t]===`[`?s(e,t,n):-1;if(!(e[t]!==`.`&&r<t)){if(i(e.slice(o,t),`object`,t),r>t){let n=e.slice(t+1,r);i(n,/^\d*$/.test(n)?`array`:`object`,r+1),t=r}o=t+1}}return i(e.slice(o),`object`,e.length),r.forEach((e,t)=>{r[t+1]?.type===`array`&&(e.default=[])}),r}function l(e){let t=e[0],n=e[1];return t.startsWith(`+`)?(t=t.slice(1),n=Number(n)):t.startsWith(`&`)?(t=t.slice(1),n=n===`on`||n===`true`||!!Number(n)):t.startsWith(`-`)&&(t=t.slice(1),n=null),{path:t,value:n}}let u=/* @__PURE__ */ new Set([`__proto__`,`constructor`,`prototype`]);function d(e){return Object.getOwnPropertyDescriptor(Object.prototype,e)!==void 0}function f(e){let t=Object.prototype.toString.call(e);return t===`[object File]`||t===`[object Blob]`}function p(e){return typeof e==`object`&&!Array.isArray(e)&&e!==null&&!f(e)}function m(e,r,a){if(e.type===`object`){let n=e.path;if(u.has(n))throw new i(e.pathToPart);if(Array.isArray(r))throw new t(e.pathToPart);let a=r;return[Object.prototype.hasOwnProperty.call(a,n)?a[n]:void 0,e=>{d(n)?Object.defineProperty(a,n,{value:e,writable:!0,enumerable:!0,configurable:!0}):a[n]=e},n]}let s=e.path!==``;if(s&&Number(e.path)>4294967294)throw new o(e.pathToPart);if(!Array.isArray(r))throw new t(e.pathToPart);let c=r,l=a.has(c);if(s&&a.add(c),!s&&l||s&&!l&&c.length>0)throw new n(e.pathToPart);let f=s?Number(e.path):c.length;return[c[f],e=>c[f]=e,f]}function h(e,{removeEmptyString:n=!1,transformEntry:r=l,depth:i=1/0}={}){if(i!==1/0&&!(Number.isInteger(i)&&i>0))throw TypeError(`depth must be a positive integer or Infinity, got ${String(i)}`);let a={},o=/* @__PURE__ */ new Set,s=[];for(let u of Array.from(e)){if(n&&u[1]===``)continue;let{path:e,value:f}=r(u,l),h=c(e,i),g=h.some(e=>e.type===`object`&&d(e.path)),_=a;h.forEach((e,n)=>{let[r,i,a]=m(e,_,o),c=_;if(h.length-1===n){if(r!==void 0)throw new t(e.pathToPart);i(f),g&&s.push([c,a,void 0])}else{if(r!==void 0&&!p(r)&&!Array.isArray(r))throw new t(e.pathToPart);let n=r??e.default;_=n,i(n),g&&r===void 0&&s.push([c,a,n])}})}for(let e=s.length-1;e>=0;e--){let[t,n,r]=s[e];r&&Object.keys(r).length>0||(Reflect.deleteProperty(t,n),Array.isArray(t)&&o.add(t))}for(let e of Array.from(o)){let t=Object.values(e);e.length=0;for(let n of t)e.push(n)}return a}e.DepthLimitError=a,e.DuplicateKeyError=t,e.ForbiddenKeyError=i,e.InvalidPathError=o,e.MixedArrayError=n,e.parseFormData=h});
8
+ //# sourceMappingURL=parse-nested-form-data.umd.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse-nested-form-data.umd.min.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @name parse-nested-form-data\n * @license MIT license.\n * @copyright (c) 2022 Christian Schurr\n * @author Christian Schurr <chris@schurr.dev>\n */\n\nimport {File} from '@remix-run/web-file'\n\n/**\n * Thrown when a path is used multiple times or has missmatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.key = key\n }\n}\n\ntype JsonObject = {[Key in string]?: JsonValue}\ntype JsonArray = Array<JsonValue>\ntype JsonValue =\n | string\n | number\n | JsonObject\n | JsonArray\n | boolean\n | null\n | File\ntype JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !(val instanceof File)\n )\n}\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nfunction defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\ntype DefaultTransform = typeof defaultTransform\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: ([path, value], defaultTransform) => {\n * return {\n * path,\n * value:\n * typeof value === 'string'\n * ? value.toUpperCase()\n * : defaultTransform(value),\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key)\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\ntype PathPart = {\n path: string\n type: 'object' | 'array'\n default: {} | []\n pathToPart: string\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @returns {Array<PathPart>} the extracted path parts\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nfunction extractPathParts(path: string): Array<PathPart> {\n const re = /((?<array>\\d*)\\]|(?<pathPart>[^.[]+))(?<nextType>\\[|\\.|$)/g\n\n return Array.from(path.matchAll(re)).map<PathPart>(match => {\n // self casted RegexExpMatchArray to custom\n const typedMatch = match as unknown as [string, string] &\n RegExpExecArray & {\n groups:\n | {\n pathPart: undefined\n array: string\n nextType: '[' | '.' | ''\n }\n | {\n pathPart: string\n array: undefined\n nextType: '[' | '.' | ''\n }\n }\n const {array, pathPart, nextType} = typedMatch.groups\n const type = array === undefined ? 'object' : 'array'\n const nextDefault = nextType === '[' ? [] : {}\n return {\n path: array ?? pathPart,\n type,\n default: nextDefault,\n pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length),\n }\n })\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`\n * and a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`.\n *\n * @param pathPart - the path part to get the setter and getter for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the setter and getter for the path part\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n] {\n if (pathPart.type === 'object') {\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n return [\n currentObject[pathPart.path],\n val => (currentObject[pathPart.path] = val),\n ]\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n const isOrdered = pathPart.path !== ''\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val)]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData, defaultTransform)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})\n * // => {a: ['foobar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := `^(\\+|\\-|\\&)?([^\\.]+?(\\[\\d*\\])*)(\\.[^\\.]+?(\\[\\d*\\])*)*$` (e.g. `+a[][1].b`)\n * - `value` := `string` or `File`\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * @returns {JsonObject} the parsed JSON object\n * @throws `DuplicateKeyError` if\n * - a path part is an object and the path part is already defined as an object\n * - a path part is an array and the path part is already defined as an array\n * @throws `MixedArrayError` if at a specific path part an unordered array is \n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n }: ParseFormDataOptions = {},\n): JsonObject {\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path)\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n }\n })\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // replace array with a squashed array\n // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])\n orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0))\n }\n\n return result\n}\n"],"names":["DuplicateKeyError","Error","constructor","key","super","this","MixedArrayError","defaultTransform","entry","path","value","startsWith","slice","Number","Boolean","extractPathParts","Array","from","matchAll","map","match","typedMatch","array","pathPart","nextType","groups","type","undefined","default","pathToPart","index","length","handlePathPart","currentPathObject","arraysWithOrder","isArray","currentObject","val","currentArray","isOrdered","isOrderedArray","has","add","order","formData","_temp","removeEmptyString","transformEntry","result","Set","pathParts","forEach","idx","nextPathValue","setNextPathValue","File","nextPathObject","orderedArray","splice","flat"],"mappings":"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"}
1
+ {"version":3,"file":"parse-nested-form-data.umd.min.js","names":["factory","global","exports","DuplicateKeyError","key","MixedArrayError","shortKey","ForbiddenKeyError","DepthLimitError","InvalidPathError","closeOf","open","lastClose","nesting","idx","path","extractPathParts","parts","addPart","type","depth","end","start","close","defaultTransform","entry","value","FORBIDDEN_OBJECT_KEYS","isPrototypeKey","isFileLike","tag","val","isJsonObject","handlePathPart","pathPart","currentPathObject","currentObject","isOrdered","currentArray","isOrderedArray","arraysWithOrder","order","parseFormData","result","createdByRemoved","formData","removeEmptyString","transformEntry","pathParts","isRemoved","nextPathValue","setNextPathValue","parent","nextPathObject","container","orderedArray","values"],"sources":["../src/errors.ts","../src/path-parts.ts","../src/transform.ts","../src/index.ts"],"sourcesContent":["// The errors `parseFormData` throws. All of them are exported from the package.\n// Each class sets `name` as a string literal: minifiers rename the classes, so\n// `this.constructor.name` would be `e` or `t` in the minified build.\n\n/**\n * Thrown when a path is used multiple times or has mismatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.name = 'DuplicateKeyError'\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.name = 'MixedArrayError'\n this.key = key\n }\n}\n/**\n * Cuts a path to 100 characters plus `...`, so a huge path from a client\n * doesn't end up in full in logs or error responses.\n */\nfunction shortKey(key: string): string {\n return key.length > 100 ? `${key.slice(0, 100)}...` : key\n}\n\n/**\n * Thrown when an object path part is `__proto__`, `constructor` or\n * `prototype`, which could modify `Object.prototype`. The names are rejected\n * at any position in the path. Entries with other names on `Object.prototype`\n * (`toString`, ...) are removed instead (see `parseFormData`).\n * `key` is the path up to the forbidden part, cut to 100 characters plus `...`\n * when it's longer.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('__proto__.polluted', 'yes')\n * parseFormData(formData)\n * // throws ForbiddenKeyError('__proto__')\n * ```\n */\nexport class ForbiddenKeyError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Forbidden key at path part ${key}`)\n this.name = 'ForbiddenKeyError'\n this.key = key\n }\n}\n/**\n * Thrown when a path has more path parts than the `depth` option allows.\n * `key` is the path up to the first path part over the limit, cut to\n * 100 characters plus `...` when it's longer.\n * Within one entry the depth is checked first, so a path that's too deep throws\n * `DepthLimitError` even if it would also throw another error, like a\n * `ForbiddenKeyError` for `__proto__`.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b[0].c', 'x')\n * parseFormData(formData, {depth: 2})\n * // throws DepthLimitError('a.b[0]')\n * ```\n */\nexport class DepthLimitError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Depth limit exceeded at path part ${key}`)\n this.name = 'DepthLimitError'\n this.key = key\n }\n}\n/**\n * Thrown when an array index in a path is larger than `4294967294`, the\n * largest index a JavaScript array can hold.\n * `key` is the path up to that array part, cut to 100 characters plus `...`\n * when it's longer.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[4294967295]', 'b')\n * parseFormData(formData)\n * // throws InvalidPathError('a[4294967295]')\n * ```\n */\nexport class InvalidPathError extends Error {\n key: string\n constructor(key: string) {\n key = shortKey(key)\n super(`Invalid path at path part ${key}`)\n this.name = 'InvalidPathError'\n this.key = key\n }\n}\n","// Splits a FormData path into its path parts.\n\nimport {DepthLimitError} from './errors'\nimport type {JsonArray, JsonObject} from './types'\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key or [key])\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\nexport type PathPart = {\n path: string\n type: 'object' | 'array'\n default: JsonObject | JsonArray\n pathToPart: string\n}\n/**\n * Finds the `]` that ends the bracket part starting at the `[` at `open`.\n * Returns -1 if this `[` is not a bracket part but a normal character of the key.\n *\n * Brackets can be nested: in `a[b[c]]` the last `]` belongs to\n * the first `[`.\n *\n * Special cases:\n * - Some `[` are never closed. The part then ends at the last `]`:\n * `a[[0]` => key `[0`.\n * - If there is no `]` after the `[` at all, the `[` is a normal character:\n * `a[0` => key `a[0`.\n * - The same goes for a `]` without a `[`: `a]b` => key `a]b`, and\n * `a[b]]` => key `]` inside `b`.\n *\n * The scan stops at the returned `]`. The caller continues from there, so no\n * character of the path is scanned twice.\n */\nfunction closeOf(path: string, open: number, lastClose: number): number {\n if (open > lastClose) return -1\n let nesting = 0\n for (let idx = open; idx < lastClose; idx++) {\n if (path[idx] === '[') nesting++\n else if (path[idx] === ']' && --nesting === 0) return idx\n }\n return lastClose\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @param depth - the maximum number of path parts (default: `Infinity`)\n * @returns {Array<PathPart>} the extracted path parts\n * @throws `DepthLimitError` if the path has more path parts than `depth`, at\n * the first part over the limit\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[b][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: [], pathToPart: 'a[b]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[b][0]'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nexport function extractPathParts(\n path: string,\n depth: number = Infinity,\n): Array<PathPart> {\n const lastClose = path.lastIndexOf(']')\n\n const parts: Array<PathPart> = []\n const addPart = (key: string, type: PathPart['type'], end: number): void => {\n // Empty keys come from `a..b`, `a.`, `.a` or a `.` or `[` after `]`.\n if (key === '' && type === 'object') return\n if (parts.length === depth) throw new DepthLimitError(path.slice(0, end))\n parts.push({path: key, type, default: {}, pathToPart: path.slice(0, end)})\n }\n\n let start = 0\n for (let idx = 0; idx < path.length; idx++) {\n const close = path[idx] === '[' ? closeOf(path, idx, lastClose) : -1\n if (path[idx] !== '.' && close < idx) continue\n\n addPart(path.slice(start, idx), 'object', idx)\n if (close > idx) {\n // Digits or nothing is an array index, anything else is the key as it\n // is (`a[b.c]` => key `b.c`).\n const key = path.slice(idx + 1, close)\n addPart(key, /^\\d*$/.test(key) ? 'array' : 'object', close + 1)\n idx = close\n }\n start = idx + 1\n }\n addPart(path.slice(start), 'object', path.length)\n\n // A part holds an array if the next part is an array index.\n parts.forEach((part, idx) => {\n if (parts[idx + 1]?.type === 'array') part.default = []\n })\n return parts\n}\n","// Turns a FormData entry into a path and a value before it is parsed.\n\nimport type {JsonLeafValue} from './types'\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nexport function defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\nexport type DefaultTransform = typeof defaultTransform\n","import {\n DepthLimitError,\n DuplicateKeyError,\n ForbiddenKeyError,\n InvalidPathError,\n MixedArrayError,\n} from './errors'\nimport {extractPathParts, type PathPart} from './path-parts'\nimport {defaultTransform, type DefaultTransform} from './transform'\nimport type {JsonArray, JsonLeafValue, JsonObject, JsonValue} from './types'\n\nexport {\n DepthLimitError,\n DuplicateKeyError,\n ForbiddenKeyError,\n InvalidPathError,\n MixedArrayError,\n}\n\n// Rejected even when a runtime doesn't define them on `Object.prototype`\n// (e.g. `node --disable-proto=delete`).\nconst FORBIDDEN_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n// The largest valid array index (2 ** 32 - 2).\nconst MAX_ARRAY_INDEX = 4_294_967_294\n\n// Any property on `Object.prototype` at parse time, including ones added by a\n// newer runtime or by other code.\nfunction isPrototypeKey(key: string): boolean {\n return Object.getOwnPropertyDescriptor(Object.prototype, key) !== undefined\n}\n\n/**\n * Detects `File` and `Blob` values by their `Symbol.toStringTag`, which works\n * for native instances, polyfills (e.g. `@remix-run/web-file`) and values from\n * other realms, where `instanceof` would fail.\n */\nfunction isFileLike(val: unknown): boolean {\n const tag = Object.prototype.toString.call(val)\n return tag === '[object File]' || tag === '[object Blob]'\n}\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !isFileLike(val)\n )\n}\n\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n * @param depth - the maximum number of path parts in a path, counting every\n * part (`a.b[0]` has 3); a positive integer or `Infinity` (default: `Infinity`).\n * For untrusted input, `32` is recommended; 2.0 will lower the default.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'x')\n * parseFormData(formData, {depth: 2})\n * // => {a: {b: 'x'}}\n * formData.append('a.c.d', 'y')\n * parseFormData(formData, {depth: 2})\n * // throws DepthLimitError('a.c.d')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: (entry, defaultTransform) => {\n * const {path, value} = defaultTransform(entry)\n * return {\n * path,\n * value: typeof value === 'string' ? value.toUpperCase() : value,\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n depth?: number\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`,\n * a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`, and the key (object key or array index) it uses.\n *\n * @param pathPart - the path part to get the value, setter and key for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the current value, a setter for it, and the key (object key or\n * array index) in `currentPathObject`\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}, key = 'a'\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject.a, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}, key = 0\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n key: string | number,\n] {\n if (pathPart.type === 'object') {\n const key = pathPart.path\n if (FORBIDDEN_OBJECT_KEYS.has(key)) {\n throw new ForbiddenKeyError(pathPart.pathToPart)\n }\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n // Only own properties are existing values, so an inherited `toString`\n // isn't read as one.\n const value = Object.prototype.hasOwnProperty.call(currentObject, key)\n ? currentObject[key]\n : undefined\n return [\n value,\n val => {\n if (!isPrototypeKey(key)) {\n currentObject[key] = val\n return\n }\n // We use `Object.defineProperty` so that read-only properties and\n // setters on the prototype are not used, but shadowed by a property\n // on our object (the prototype stays untouched). The entry is removed\n // at the end (see `parseFormData`).\n Object.defineProperty(currentObject, key, {\n value: val,\n writable: true,\n enumerable: true,\n configurable: true,\n })\n },\n key,\n ]\n }\n const isOrdered = pathPart.path !== ''\n // `path` only contains digits here, so `Number` never returns NaN. Indices\n // too large to be safe integers are above the limit as well.\n if (isOrdered && Number(pathPart.path) > MAX_ARRAY_INDEX) {\n throw new InvalidPathError(pathPart.pathToPart)\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val), order]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[key]` after path part => nest into `objects`, same as `.key`; the text\n * in brackets is the key as it is (`a[b.c]` => key `b.c`)\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[b]', 'foo')\n * formData.append('a.c', 'bar')\n * parseFormData(formData)\n * // => {a: {b: 'foo', c: 'bar'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {\n * transformEntry: ([path, value]) => ({\n * path,\n * value: typeof value === 'string' ? value + 'bar' : value,\n * }),\n * })\n * // => {a: ['foobar']}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := an optional prefix (`+`, `-`, `&`), then path parts: keys after\n * `.` or in brackets (`[key]`), and array parts `[]` or `[\\d+]` (e.g.\n * `+a[][1].b[c]`)\n * - `value` := `string` or `File` (native or polyfilled `File` values are kept\n * as they are)\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * - `depth` - the maximum number of path parts in a path, counting every part\n * (default: `Infinity`)\n * @returns {JsonObject} the parsed JSON object\n * @throws `TypeError` if `depth` isn't a positive integer or `Infinity`\n * @throws `DepthLimitError` if a path has more path parts than `depth`\n * - e.g. `a.b[0]` with `depth: 2` => `DepthLimitError('a.b[0]')`\n * @throws `DuplicateKeyError` if\n * - two entries set a value at the same path\n * - e.g. `a` and `a`, `a[0]` and `a[0]`, `a.b` and `a[b]`\n * - an entry uses a path part as an object, array or value and another entry\n * uses it differently\n * - e.g. `a` and `a.b`, `a.b` and `a[]`, `a[]` and `a.b`\n * - the path starts with an array part\n * - e.g. `[0]` or `[]`\n * @throws `MixedArrayError` if at a specific path part an unordered array is\n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n * @throws `ForbiddenKeyError` if an object path part is `__proto__`,\n * `constructor` or `prototype`\n * - e.g. `__proto__.polluted`\n * @throws `InvalidPathError` if an array index is larger than `4294967294`\n * - e.g. `a[4294967295]`\n *\n * Every error's `key` is the path after `transformEntry`, not the field name\n * (e.g. `+a.b.c` with `depth: 2` => `DepthLimitError('a.b.c')`).\n *\n * An entry with any other object path part named like a property on\n * `Object.prototype` (`toString`, `hasOwnProperty`, ...) is removed from the\n * result. It's parsed like any other entry first, so it causes the same errors\n * as an unused name: an error doesn't tell the client which properties exist\n * on the server's `Object.prototype`.\n * - e.g. `user.toString` => `{}`\n * - e.g. `user[toString]` => `{}`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n depth = Infinity,\n }: ParseFormDataOptions = {},\n): JsonObject {\n if (depth !== Infinity && !(Number.isInteger(depth) && depth > 0)) {\n throw new TypeError(\n `depth must be a positive integer or Infinity, got ${String(depth)}`,\n )\n }\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n // What the removed entries created, in order. `container` is the object or\n // array an entry created on its way to its value; it's undefined for the\n // value itself.\n const createdByRemoved: Array<\n [\n parent: JsonObject | JsonArray,\n key: string | number,\n container: JsonObject | JsonArray | undefined,\n ]\n > = []\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path, depth)\n // Removed entries are still built like any other entry, so they cause the\n // same errors as an unused name, and undone below.\n const isRemoved = pathParts.some(\n pathPart => pathPart.type === 'object' && isPrototypeKey(pathPart.path),\n )\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue, key] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n const parent = currentPathObject\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n if (isRemoved) createdByRemoved.push([parent, key, undefined])\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n if (isRemoved && nextPathValue === undefined) {\n createdByRemoved.push([parent, key, nextPathObject])\n }\n }\n })\n }\n\n // Delete what the removed entries added. Go backwards so children are\n // deleted before their parents. A container that still has keys is also\n // used by a kept entry, so it stays.\n for (let idx = createdByRemoved.length - 1; idx >= 0; idx--) {\n const [parent, key, container] = createdByRemoved[idx]!\n if (container && Object.keys(container).length > 0) continue\n Reflect.deleteProperty(parent, key)\n // The hole is squashed below like the gaps in ordered arrays.\n if (Array.isArray(parent)) arraysWithOrder.add(parent)\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // Squash the sparse array in place (e.g. [0, , 1] => [0, 1]).\n // `Object.values` only visits slots that are set, in ascending index\n // order, so the cost depends on the number of entries, not on `length`.\n const values = Object.values(orderedArray)\n orderedArray.length = 0\n for (const value of values) orderedArray.push(value)\n }\n\n return result\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,77 +1,132 @@
1
1
  {
2
2
  "name": "parse-nested-form-data",
3
- "version": "1.0.0",
4
- "description": "A tiny node module for parsing FormData by name into objects and arrays",
5
- "main": "dist/parse-nested-form-data.cjs.js",
6
- "module": "dist/parse-nested-form-data.esm.js",
7
- "types": "dist/index.d.ts",
3
+ "version": "1.1.0",
4
+ "description": "Tiny (under 2 kB), dependency-free parser that turns FormData into nested objects and arrays, based on the field names. Works in browsers, Node.js, Deno, Bun and edge runtimes.",
8
5
  "keywords": [
9
6
  "FormData",
10
- "parse",
11
- "nested",
12
- "form",
13
7
  "array",
8
+ "form",
9
+ "form-data",
10
+ "lightweight",
11
+ "multipart",
12
+ "nested",
13
+ "nextjs",
14
+ "no-dependencies",
14
15
  "object",
15
- "parse"
16
+ "parse",
17
+ "parser",
18
+ "react-router",
19
+ "remix",
20
+ "server-actions",
21
+ "tiny",
22
+ "typescript",
23
+ "urlsearchparams",
24
+ "zero-dependency"
16
25
  ],
17
- "author": "Christian Schurr <chris@schurr.dev>",
26
+ "homepage": "https://github.com/milamer/parse-nested-form-data#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/milamer/parse-nested-form-data/issues"
29
+ },
18
30
  "license": "MIT",
31
+ "author": "Christian Schurr <chris@schurr.dev>",
19
32
  "repository": {
20
33
  "type": "git",
21
- "url": "https://github.com/milamer/parse-nested-form-data"
34
+ "url": "git+https://github.com/milamer/parse-nested-form-data.git"
22
35
  },
23
- "bugs": {
24
- "url": "https://github.com/milamer/parse-nested-form-data/issues"
25
- },
26
- "homepage": "https://github.com/milamer/parse-nested-form-data#readme",
27
36
  "files": [
28
37
  "dist"
29
38
  ],
39
+ "sideEffects": false,
40
+ "main": "dist/parse-nested-form-data.cjs.js",
41
+ "module": "dist/parse-nested-form-data.esm.js",
42
+ "types": "dist/index.d.ts",
30
43
  "scripts": {
31
- "clean": "rimraf dist coverage node_modules",
32
- "build:toc": "npx doctoc .",
33
- "build": "kcd-scripts build --bundle --environment BUILD_NAME:parseNestedFormData",
34
- "lint": "kcd-scripts lint",
35
- "setup": "npm install && npm run validate -s",
36
- "test": "kcd-scripts test",
37
- "typecheck": "kcd-scripts typecheck",
38
- "test:update": "npm test -- --updateSnapshot --coverage",
39
- "validate": "kcd-scripts validate"
40
- },
41
- "dependencies": {
42
- "@babel/runtime": "^7.19.0"
44
+ "clean": "node -e \"for (const d of ['dist','coverage']) require('fs').rmSync(d,{recursive:true,force:true})\"",
45
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsdown",
46
+ "build:toc": "doctoc README.md && oxfmt README.md",
47
+ "format": "oxfmt",
48
+ "format:check": "oxfmt --check",
49
+ "lint": "oxlint --type-aware --deny-warnings",
50
+ "typecheck": "tsc -p .",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "test:coverage": "vitest run --coverage",
54
+ "knip": "knip",
55
+ "check:package": "publint && attw --pack .",
56
+ "size": "size-limit",
57
+ "validate": "npm run format:check && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:dist && npm run check:package && npm run size && npm run knip",
58
+ "test:dist": "node scripts/smoke-dist.mjs"
43
59
  },
44
60
  "devDependencies": {
61
+ "@arethetypeswrong/cli": "^0.18.5",
62
+ "@commitlint/cli": "^21.2.4",
63
+ "@commitlint/config-conventional": "^21.2.4",
45
64
  "@remix-run/web-file": "^3.0.2",
46
- "@remix-run/web-form-data": "^3.0.3",
47
- "kcd-scripts": "^12.3.0",
48
- "rimraf": "^3.0.2",
49
- "typescript": "^4.8.4"
65
+ "@size-limit/preset-small-lib": "^14.3.0",
66
+ "@vitest/coverage-v8": "^5.1.0",
67
+ "doctoc": "^2.5.0",
68
+ "fast-check": "^4.10.2",
69
+ "knip": "^6.44.1",
70
+ "lefthook": "^2.2.2",
71
+ "oxfmt": "^0.76.0",
72
+ "oxlint": "^1.91.0",
73
+ "oxlint-tsgolint": "^7.0.2006",
74
+ "publint": "^0.3.25",
75
+ "size-limit": "^14.3.0",
76
+ "tsdown": "^0.23.5",
77
+ "typescript": "7.0.2",
78
+ "vitest": "^5.1.0"
50
79
  },
51
- "peerDependencies": {
52
- "@remix-run/web-file": ">=3.0.0"
80
+ "commitlint": {
81
+ "extends": [
82
+ "@commitlint/config-conventional"
83
+ ]
53
84
  },
54
- "peerDependenciesMeta": {
55
- "@remix-run/web-form-data": {
56
- "optional": true
57
- }
58
- },
59
- "eslintConfig": {
60
- "extends": "./node_modules/kcd-scripts/eslint.js",
61
- "rules": {
62
- "import/extensions": "off",
63
- "max-lines-per-function": "off",
64
- "one-var": [
65
- "error",
66
- "never"
85
+ "release": {
86
+ "branches": [
87
+ "+([0-9]).x",
88
+ "+([0-9]).+([0-9]).x",
89
+ "main"
90
+ ],
91
+ "plugins": [
92
+ [
93
+ "@semantic-release/commit-analyzer",
94
+ {
95
+ "preset": "conventionalcommits"
96
+ }
97
+ ],
98
+ [
99
+ "@semantic-release/release-notes-generator",
100
+ {
101
+ "preset": "conventionalcommits"
102
+ }
103
+ ],
104
+ [
105
+ "@semantic-release/npm",
106
+ {
107
+ "npmPublish": false
108
+ }
67
109
  ],
68
- "@typescript-eslint/no-unsafe-assignment": "off",
69
- "@typescript-eslint/no-loop-func": "off"
110
+ [
111
+ "@semantic-release/exec",
112
+ {
113
+ "publishCmd": "npm stage publish --tag ${nextRelease.channel ? 'release-' + nextRelease.channel : 'latest'}"
114
+ }
115
+ ],
116
+ "@semantic-release/github"
117
+ ]
118
+ },
119
+ "devEngines": {
120
+ "packageManager": {
121
+ "name": "npm",
122
+ "version": ">=11.19.1",
123
+ "onFail": "error"
70
124
  }
71
125
  },
72
- "eslintIgnore": [
73
- "node_modules",
74
- "coverage",
75
- "dist"
76
- ]
126
+ "packageManager": "npm@11.19.1+sha512.cedb312b1b7f92421a02cfb68b4194e88f8346651dacd6acc5364a25ef5309d4a32b19616a1ff8aff865e7280c0fa5c0835a99f5cd93368991e2a80ec9da75d2",
127
+ "allowScripts": {
128
+ "lefthook": false,
129
+ "@tybys/wasm-util": false,
130
+ "fsevents": false
131
+ }
77
132
  }
package/CHANGELOG.md DELETED
@@ -1,13 +0,0 @@
1
- <!-- START doctoc generated TOC please keep comment here to allow auto update -->
2
- <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3
- **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
4
-
5
- - [CHANGELOG](#changelog)
6
-
7
- <!-- END doctoc generated TOC please keep comment here to allow auto update -->
8
-
9
- # CHANGELOG
10
-
11
- The changelog is automatically updated using
12
- [semantic-release](https://github.com/semantic-release/semantic-release). You
13
- can see it on the [releases page](../../releases).
@@ -1 +0,0 @@
1
- export {};