diff --git a/changelog.md b/changelog.md index 8b6a08e..f5cdee3 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,9 @@ ### Minor +- Added the ability to customize what are extractable files, fixing [#10](https://github.com/jaydenseric/extract-files/issues/10) via [#11](https://github.com/jaydenseric/extract-files/pull/11): + - Added a new third parameter to the `extractFiles` function, for specifying a custom extractable file matcher. + - Export a new `isExtractableFile` function that is used as the default extractable file matcher for the `extractFiles` function. This can be used in a custom extractable file matcher implementation to match the default extractable files, along with additional custom files. - Setup [GitHub Sponsors funding](https://github.com/sponsors/jaydenseric): - Added `.github/funding.yml` to display a sponsor button in GitHub. - Added a `package.json` `funding` field to enable npm CLI funding features. diff --git a/readme.md b/readme.md index 6dbb545..dad15bf 100644 --- a/readme.md +++ b/readme.md @@ -28,7 +28,9 @@ See the [`extractFiles`](#function-extractfiles) documentation to get started. - [class ReactNativeFile](#class-reactnativefile) - [function extractFiles](#function-extractfiles) +- [function isExtractableFile](#function-isextractablefile) - [type ExtractableFile](#type-extractablefile) +- [type ExtractableFileMatcher](#type-extractablefilematcher) - [type ExtractFilesResult](#type-extractfilesresult) - [type ObjectPath](#type-objectpath) - [type ReactNativeFileSubstitute](#type-reactnativefilesubstitute) @@ -65,6 +67,7 @@ Clones a value, recursively extracting [`File`](https://developer.mozilla.org/do | :-- | :-- | :-- | | `value` | \* | Value (typically an object tree) to extract files from. | | `path` | [ObjectPath](#type-objectpath)? = `''` | Prefix for object paths for extracted files. | +| `isExtractableFile` | [ExtractableFileMatcher](#type-extractablefilematcher)? = [isExtractableFile](#function-isextractablefile) | The function used to identify extractable files. | **Returns:** [ExtractFilesResult](#type-extractfilesresult) — Result. @@ -107,6 +110,28 @@ _Extract files from an object._ --- +### function isExtractableFile + +Checks if a value is an [extractable file](#type-extractablefile). + +**Type:** [ExtractableFileMatcher](#type-extractablefilematcher) + +| Parameter | Type | Description | +| :-------- | :--- | :-------------- | +| `value` | \* | Value to check. | + +**Returns:** boolean — Is the value an [extractable file](#type-extractablefile). + +#### Examples + +_How to import._ + +> ```js +> import { isExtractableFile } from 'extract-files' +> ``` + +--- + ### type ExtractableFile An extractable file. @@ -115,6 +140,36 @@ An extractable file. --- +### type ExtractableFileMatcher + +A function that checks if a value is an extractable file. + +**Type:** Function + +| Parameter | Type | Description | +| :-------- | :--- | :-------------- | +| `value` | \* | Value to check. | + +**Returns:** boolean — Is the value an extractable file. + +#### See + +- [`isExtractableFile`](#function-isextractablefile) is the default extractable file matcher. + +#### Examples + +_How to check for the default exactable files, as well as a custom type of file._ + +> ```js +> import { isExtractableFile } from 'extract-files' +> +> const isExtractableFileEnhanced = value => +> isExtractableFile(value) || +> (typeof CustomFile !== 'undefined' && value instanceof CustomFile) +> ``` + +--- + ### type ExtractFilesResult What [`extractFiles`](#function-extractfiles) returns. diff --git a/src/extractFiles.mjs b/src/extractFiles.mjs index 7ac66e7..d3da221 100644 --- a/src/extractFiles.mjs +++ b/src/extractFiles.mjs @@ -1,4 +1,4 @@ -import { ReactNativeFile } from './ReactNativeFile.mjs' +import { isExtractableFile as defaultIsExtractableFile } from './isExtractableFile.mjs' /** * Clones a value, recursively extracting @@ -13,6 +13,7 @@ import { ReactNativeFile } from './ReactNativeFile.mjs' * @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 Extract files from an object. * For the following: @@ -48,7 +49,11 @@ import { ReactNativeFile } from './ReactNativeFile.mjs' * | `file1` | `['prefix.a', 'prefix.b.0']` | * | `file2` | `['prefix.b.1']` | */ -export function extractFiles(value, path = '') { +export function extractFiles( + value, + path = '', + isExtractableFile = defaultIsExtractableFile +) { let clone const files = new Map() @@ -66,11 +71,7 @@ export function extractFiles(value, path = '') { else files.set(file, paths) } - if ( - (typeof File !== 'undefined' && value instanceof File) || - (typeof Blob !== 'undefined' && value instanceof Blob) || - value instanceof ReactNativeFile - ) { + if (isExtractableFile(value)) { clone = null addFile([path], value) } else { @@ -83,14 +84,18 @@ export function extractFiles(value, path = '') { }) else if (Array.isArray(value)) clone = value.map((child, i) => { - const result = extractFiles(child, `${prefix}${i}`) + const result = extractFiles(child, `${prefix}${i}`, isExtractableFile) result.files.forEach(addFile) return result.clone }) else if (value && value.constructor === Object) { clone = {} for (const i in value) { - const result = extractFiles(value[i], `${prefix}${i}`) + const result = extractFiles( + value[i], + `${prefix}${i}`, + isExtractableFile + ) result.files.forEach(addFile) clone[i] = result.clone } diff --git a/src/index.mjs b/src/index.mjs index ef37740..4a6096f 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,5 +1,6 @@ export { extractFiles } from './extractFiles.mjs' export { ReactNativeFile } from './ReactNativeFile.mjs' +export { isExtractableFile } from './isExtractableFile.mjs' /** * An extractable file. @@ -8,6 +9,24 @@ export { ReactNativeFile } from './ReactNativeFile.mjs' * @type {File|Blob|ReactNativeFile} */ +/** + * A function that checks if a value is an extractable file. + * @kind typedef + * @name ExtractableFileMatcher + * @type {Function} + * @param {*} value Value to check. + * @returns {boolean} Is the value an extractable file. + * @see [`isExtractableFile`]{@link isExtractableFile} is the default extractable file matcher. + * @example How to check for the default exactable files, as well as a custom type of file. + * ```js + * import { isExtractableFile } from 'extract-files' + * + * const isExtractableFileEnhanced = value => + * isExtractableFile(value) || + * (typeof CustomFile !== 'undefined' && value instanceof CustomFile) + * ``` + */ + /** * What [`extractFiles`]{@link extractFiles} returns. * @kind typedef diff --git a/src/isExtractableFile.mjs b/src/isExtractableFile.mjs new file mode 100644 index 0000000..6bd805a --- /dev/null +++ b/src/isExtractableFile.mjs @@ -0,0 +1,18 @@ +import { ReactNativeFile } from './ReactNativeFile.mjs' + +/** + * Checks if a value is an [extractable file]{@link ExtractableFile}. + * @kind function + * @name isExtractableFile + * @type {ExtractableFileMatcher} + * @param {*} value Value to check. + * @returns {boolean} Is the value an [extractable file]{@link ExtractableFile}. + * @example How to import. + * ```js + * import { isExtractableFile } from 'extract-files' + * ``` + */ +export const isExtractableFile = value => + (typeof File !== 'undefined' && value instanceof File) || + (typeof Blob !== 'undefined' && value instanceof Blob) || + value instanceof ReactNativeFile diff --git a/src/test.mjs b/src/test.mjs index 55358bd..91442a7 100644 --- a/src/test.mjs +++ b/src/test.mjs @@ -1,8 +1,21 @@ import t from 'tap' import { extractFiles } from './extractFiles.mjs' +import { isExtractableFile } from './isExtractableFile.mjs' import { ReactNativeFile } from './ReactNativeFile.mjs' -t.test('Extracts a file value.', t => { +t.test('`isExtractableFile` matches a file', t => { + const file = new ReactNativeFile({ name: '', type: '', uri: '' }) + t.equal(isExtractableFile(file), true) + t.end() +}) + +t.test('`isExtractableFile` doesn’t match a non-file', t => { + const notAFile = {} + t.equal(isExtractableFile(notAFile), false) + t.end() +}) + +t.test('`extractFiles` extracts a file value.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -18,7 +31,7 @@ t.test('Extracts a file value.', t => { }) t.test( - 'Extracts File instances from a FileList instance in an object value.', + '`extractFiles` extracts `File` instances from a `FileList` instance in an object value.', t => { const originalFile = global.File const originalFileList = global.FileList @@ -56,7 +69,7 @@ t.test( } ) -t.test('Extracts a File instance in an object value.', t => { +t.test('`extractFiles` extracts a `File` instance in an object value.', t => { const original = global.File global.File = class File {} const file = new File() @@ -75,7 +88,7 @@ t.test('Extracts a File instance in an object value.', t => { t.end() }) -t.test('Extracts a Blob instance in an object value.', t => { +t.test('`extractFiles` extracts a `Blob` instance in an object value.', t => { const original = global.Blob global.Blob = class Blob {} const file = new Blob() @@ -94,22 +107,25 @@ t.test('Extracts a Blob instance in an object value.', t => { t.end() }) -t.test('Extracts a ReactNativeFile instance in an object value.', t => { - const file = new ReactNativeFile({ name: '', type: '', uri: '' }) +t.test( + '`extractFiles` extracts a `ReactNativeFile` instance in an object value.', + t => { + const file = new ReactNativeFile({ name: '', type: '', uri: '' }) - t.strictDeepEqual( - extractFiles({ a: file }), - { - clone: { a: null }, - files: new Map([[file, ['a']]]) - }, - 'Result.' - ) + t.strictDeepEqual( + extractFiles({ a: file }), + { + clone: { a: null }, + files: new Map([[file, ['a']]]) + }, + 'Result.' + ) - t.end() -}) + t.end() + } +) -t.test('Extracts files from an array value.', t => { +t.test('`extractFiles` extracts files from an array value.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -124,7 +140,7 @@ t.test('Extracts files from an array value.', t => { t.end() }) -t.test('Extracts files from a nested array value.', t => { +t.test('`extractFiles` extracts files from a nested array value.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -139,7 +155,7 @@ t.test('Extracts files from a nested array value.', t => { t.end() }) -t.test('Extracts files in an object value.', t => { +t.test('`extractFiles` extracts files in an object value.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -154,7 +170,7 @@ t.test('Extracts files in an object value.', t => { t.end() }) -t.test('Extracts files from a nested object value.', t => { +t.test('`extractFiles` extracts files from a nested object value.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -169,7 +185,7 @@ t.test('Extracts files from a nested object value.', t => { t.end() }) -t.test('Extracts files with a path.', t => { +t.test('`extractFiles` extracts files with a path.', t => { const file = new ReactNativeFile({ name: '', type: '', uri: '' }) t.strictDeepEqual( @@ -184,7 +200,7 @@ t.test('Extracts files with a path.', t => { t.end() }) -t.test('Handles an undefined value.', t => { +t.test('`extractFiles` handles an undefined value.', t => { t.strictDeepEqual( extractFiles(undefined), { clone: undefined, files: new Map() }, @@ -193,7 +209,7 @@ t.test('Handles an undefined value.', t => { t.end() }) -t.test('Handles a null value.', t => { +t.test('`extractFiles` handles a null value.', t => { t.strictDeepEqual( extractFiles(null), { clone: null, files: new Map() }, @@ -202,7 +218,7 @@ t.test('Handles a null value.', t => { t.end() }) -t.test('Handles an instance value.', t => { +t.test('`extractFiles` handles an instance value.', t => { const dateInstance = new Date(2019, 0, 20) t.strictDeepEqual( @@ -213,7 +229,7 @@ t.test('Handles an instance value.', t => { t.end() }) -t.test('Handles an empty object value.', t => { +t.test('`extractFiles` handles an empty object value.', t => { t.strictDeepEqual( extractFiles({}), { clone: {}, files: new Map() }, @@ -222,35 +238,21 @@ t.test('Handles an empty object value.', t => { t.end() }) -t.test('Handles an object value with various property types.', t => { - const func = () => {} - const dateInstance = new Date(2019, 0, 20) - const numberInstance = new Number(1) - class Class { - a = true - } - const classInstance = new Class() - const objectInstance = new Object() - objectInstance.a = true +t.test( + '`extractFiles` handles an object value with various property types.', + t => { + const func = () => {} + const dateInstance = new Date(2019, 0, 20) + const numberInstance = new Number(1) + class Class { + a = true + } + const classInstance = new Class() + const objectInstance = new Object() + objectInstance.a = true - t.strictDeepEqual( - extractFiles({ - a: '', - b: 'a', - c: 0, - d: 1, - e: true, - f: false, - g: null, - h: undefined, - i: func, - j: objectInstance, - k: classInstance, - l: numberInstance, - m: dateInstance - }), - { - clone: { + t.strictDeepEqual( + extractFiles({ a: '', b: 'a', c: 0, @@ -264,8 +266,60 @@ t.test('Handles an object value with various property types.', t => { k: classInstance, l: numberInstance, m: dateInstance + }), + { + clone: { + a: '', + b: 'a', + c: 0, + d: 1, + e: true, + f: false, + g: null, + h: undefined, + i: func, + j: objectInstance, + k: classInstance, + l: numberInstance, + m: dateInstance + }, + files: new Map() + }, + 'Result.' + ) + + t.end() + } +) + +t.test('`extractFiles` allows overriding `isExtractableFile`.', t => { + class CustomFile {} + + const isExtractableFile = value => value instanceof CustomFile + + const file1 = new CustomFile() + const file2 = new CustomFile() + const file3 = new CustomFile() + + t.strictDeepEqual( + extractFiles( + { + a: file1, + b: [file2, { c: file3 }] + }, + '', + isExtractableFile + ), + { + clone: { + a: null, + b: [null, { c: null }] }, - files: new Map() + files: new Map([ + [file1, ['a']], + [file2, ['b.0']], + [file3, ['b.1.c']] + ]) }, 'Result.' )