'use strict'; const defaultIsExtractableFile = require('./isExtractableFile.js'); /** * Clones a value, recursively extracting * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File), * [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and * [`ReactNativeFile`]{@link ReactNativeFile} instances with their * [object paths]{@link ObjectPath}, replacing them with `null`. * [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist) instances * are treated as [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) * instance arrays. * @kind function * @name extractFiles * @param {*} value Value (typically an object tree) to extract files from. * @param {ObjectPath} [path=''] Prefix for object paths for extracted files. * @param {ExtractableFileMatcher} [isExtractableFile=isExtractableFile] The function used to identify extractable files. * @returns {ExtractFilesResult} Result. * @example Ways to `import`. * ```js * import { extractFiles } from 'extract-files'; * ``` * * ```js * import extractFiles from 'extract-files/public/extractFiles.js'; * ``` * @example Ways to `require`. * ```js * const { extractFiles } = require('extract-files'); * ``` * * ```js * const extractFiles = require('extract-files/public/extractFiles.js'); * ``` * @example Extract files from an object. * For the following: * * ```js * const file1 = new File(['1'], '1.txt', { type: 'text/plain' }); * const file2 = new File(['2'], '2.txt', { type: 'text/plain' }); * const value = { * a: file1, * b: [file1, file2], * }; * * const { clone, files } = extractFiles(value, 'prefix'); * ``` * * `value` remains the same. * * `clone` is: * * ```json * { * "a": null, * "b": [null, null] * } * ``` * * `files` is a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instance containing: * * | Key | Value | * | :------ | :--------------------------- | * | `file1` | `['prefix.a', 'prefix.b.0']` | * | `file2` | `['prefix.b.1']` | */ module.exports = function extractFiles( value, path = '', isExtractableFile = defaultIsExtractableFile ) { // Map of extracted files and their object paths within the input value. const files = new Map(); // Map of arrays and objects recursed within the input value and their clones, // for reusing clones of values that are referenced multiple times within the // input value. const clones = new Map(); /** * Recursively clones the value, extracting files. * @kind function * @name extractFiles~recurse * @param {*} value Value to extract files from. * @param {ObjectPath} path Prefix for object paths for extracted files. * @param {Set} recursed Recursed arrays and objects for avoiding infinite recursion of circular references within the input value. * @returns {*} Clone of the value with files replaced with `null`. * @ignore */ function recurse(value, path, recursed) { let clone = value; if (isExtractableFile(value)) { clone = null; const filePaths = files.get(value); filePaths ? filePaths.push(path) : files.set(value, [path]); } else { const isList = Array.isArray(value) || (typeof FileList !== 'undefined' && value instanceof FileList); const isObject = value && value.constructor === Object; if (isList || isObject) { const hasClone = clones.has(value); if (hasClone) clone = clones.get(value); else { clone = isList ? [] : {}; clones.set(value, clone); } if (!recursed.has(value)) { const pathPrefix = path ? `${path}.` : ''; const recursedDeeper = new Set(recursed).add(value); if (isList) { let index = 0; for (const item of value) { const itemClone = recurse( item, pathPrefix + index++, recursedDeeper ); if (!hasClone) clone.push(itemClone); } } else for (const key in value) { const propertyClone = recurse( value[key], pathPrefix + key, recursedDeeper ); if (!hasClone) clone[key] = propertyClone; } } } } return clone; } return { clone: recurse(value, path, new Set()), files, }; };