Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ jobs:
with:
file: ./coverage/lcov.info

size:
name: Check bundle size
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24

- name: Install dependencies
run: npm ci

# Runs size-limit (builds first via the `presize` script), failing if the main entry exceeds
# its budget — e.g. if the full MIME table leaks back out of the `file-selector/mime` subpath.
- name: Check bundle size
run: npm run size

e2e:
name: Run e2e (browser) tests
runs-on: ubuntu-latest
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ document.addEventListener('drop', async evt => {
});
```

### MIME types
When the browser doesn't set a File's `type`, `fromEvent` infers one from the file extension. By default it uses a small built-in table of the most common types, keeping the bundle lean.

If you need broader coverage, import the full extension-to-MIME table from the `file-selector/mime` subpath and pass it via the `mimeTypes` option. Because it's a separate entry point, the full table (~1,200 entries) is only included in your bundle when you import it:
```ts
import {fromEvent} from 'file-selector';
import {COMMON_MIME_TYPES} from 'file-selector/mime';

const files = await fromEvent(evt, {mimeTypes: COMMON_MIME_TYPES});
```

You can also pass your own `Map<extension, mimeType>` to restrict or extend the lookup:
```ts
const files = await fromEvent(evt, {mimeTypes: new Map([['dwg', 'image/vnd.dwg']])});
```


## Browser Support
Most browser support basic File selection with drag 'n' drop or file input:
Expand Down
76 changes: 76 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./mime": {
"types": "./dist/mime.d.ts",
"import": "./dist/mime.js",
"require": "./dist/mime.cjs"
},
"./package.json": "./package.json"
},
"sideEffects": false,
Expand Down Expand Up @@ -55,21 +60,37 @@
"test": "vitest",
"test:cov": "vitest run --coverage",
"pretest:e2e": "npm run build",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"presize": "npm run build",
"size": "size-limit"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@size-limit/file": "^12.1.0",
"@types/node": "^26.1.1",
"@vitest/coverage-v8": "^4.1.10",
"jsdom": "^29.1.1",
"oxfmt": "^0.58.0",
"oxlint": "^1.73.0",
"oxlint-tsgolint": "^0.24.0",
"size-limit": "^12.1.0",
"tsdown": "^0.22.4",
"typescript": "^7.0.2",
"vitest": "^4.1.10"
},
"engines": {
"node": ">= 20"
}
},
"size-limit": [
{
"name": "core, esm (fromEvent + default MIME map)",
"path": ["dist/index.js", "dist/mime-default.js"],
"limit": "3 kB"
},
{
"name": "core, cjs (fromEvent + default MIME map)",
"path": ["dist/index.cjs", "dist/mime-default.cjs"],
"limit": "4 kB"
}
]
}
32 changes: 32 additions & 0 deletions src/file-selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,38 @@ it('should fallback to getAsFile when getAsFileSystemHandle resolves to undefine
expect(file.path).toBe(`./${name}`);
});

it('guesses the {type} from the extension using the built-in defaults', async () => {
const mockFile = createFile('test.png', {ping: true}); // typeless File
const evt = inputEvtFromFiles(mockFile);

const [file] = (await fromEvent(evt)) as FileWithPath[];
expect(file.type).toBe('image/png');
});

it('does not guess the {type} for extensions outside the built-in defaults', async () => {
const mockFile = createFile('test.3mf', {ping: true}); // uncommon, not in DEFAULT_MIME_TYPES
const evt = inputEvtFromFiles(mockFile);

const [file] = (await fromEvent(evt)) as FileWithPath[];
expect(file.type).toBe('');
});

it('guesses the {type} from a custom {mimeTypes} lookup when provided', async () => {
const mockFile = createFile('test.3mf', {ping: true}); // typeless File
const evt = inputEvtFromFiles(mockFile);

const [file] = (await fromEvent(evt, {mimeTypes: new Map([['3mf', 'model/3mf']])})) as FileWithPath[];
expect(file.type).toBe('model/3mf');
});

it('does not overwrite a {type} already set by the browser', async () => {
const mockFile = createFile('test.png', {ping: true}, {type: 'application/octet-stream'});
const evt = inputEvtFromFiles(mockFile);

const [file] = (await fromEvent(evt)) as FileWithPath[];
expect(file.type).toBe('application/octet-stream');
});

function dragEvtFromItems(items: DataTransferItem | DataTransferItem[], type: string = 'drop'): DragEvent {
return {
type,
Expand Down
35 changes: 33 additions & 2 deletions src/file-selector.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import {UnexpectedObjectError} from './error';
import {type FileWithPath, toFileWithPath} from './file';
import {type FileWithPath, toFileWithPath, withMimeType} from './file';
import {DEFAULT_MIME_TYPES} from './mime-default';

const FILES_TO_IGNORE = [
// Thumbnail cache files for macOS and Windows
'.DS_Store', // macOs
'Thumbs.db' // Windows
];

export interface FromEventOptions {
/**
* Extension-to-MIME lookup used to set `type` on files the browser left typeless.
* Defaults to a small built-in set of common types ({@link DEFAULT_MIME_TYPES}).
*
* The full extension-to-MIME table (~1,200 entries) is not bundled into the main entry;
* import it from the `file-selector/mime` subpath when broader coverage is needed:
*
* ```ts
* import {fromEvent} from 'file-selector';
* import {COMMON_MIME_TYPES} from 'file-selector/mime';
* await fromEvent(evt, {mimeTypes: COMMON_MIME_TYPES});
* ```
*
* See https://github.com/react-dropzone/file-selector/issues/127
*/
mimeTypes?: Map<string, string>;
}

/**
* Convert a DragEvent's DataTrasfer object to a list of File objects
* NOTE: If some of the items are folders,
Expand All @@ -16,8 +36,19 @@ const FILES_TO_IGNORE = [
* and a list of File objects will be returned.
*
* @param evt
* @param options
*/
export async function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
export async function fromEvent(
evt: Event | any,
{mimeTypes = DEFAULT_MIME_TYPES}: FromEventOptions = {}
): Promise<(FileWithPath | DataTransferItem)[]> {
const items = await getFilesOrItems(evt);
// Guess a MIME type from the file extension once, over the final flat list, for any file the
// browser left typeless. DataTransferItems (returned for non-'drop' drag events) pass through as-is.
return items.map(item => (item instanceof File ? withMimeType(item, mimeTypes) : item));
}

async function getFilesOrItems(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
if (isObject<DragEvent>(evt) && isDataTransfer(evt.dataTransfer)) {
return getDataTransferFiles(evt.dataTransfer, evt.type);
} else if (isChangeEvt(evt)) {
Expand Down
10 changes: 6 additions & 4 deletions src/file.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {COMMON_MIME_TYPES, toFileWithPath} from './file';
import {toFileWithPath, withMimeType} from './file';
import {COMMON_MIME_TYPES} from './mime';
import {DEFAULT_MIME_TYPES} from './mime-default';

describe('toFile()', () => {
it('should be an instance of a File', () => {
Expand Down Expand Up @@ -114,7 +116,7 @@ describe('toFile()', () => {
const types = Array.from(COMMON_MIME_TYPES.values());
const files = Array.from(COMMON_MIME_TYPES.keys())
.map(ext => new File([], `test.${ext}`))
.map(f => toFileWithPath(f));
.map(f => withMimeType(f, COMMON_MIME_TYPES));

for (const file of files) {
expect(types.includes(file.type)).toBe(true);
Expand All @@ -123,7 +125,7 @@ describe('toFile()', () => {

test('{type} is enumerable', () => {
const file = new File([], 'test.gif');
const fileWithPath = toFileWithPath(file);
const fileWithPath = withMimeType(toFileWithPath(file), DEFAULT_MIME_TYPES);

expect(Object.keys(fileWithPath)).toContain('type');

Expand All @@ -140,7 +142,7 @@ describe('toFile()', () => {
const files = Array.from(COMMON_MIME_TYPES.keys())
.map(key => key.toUpperCase())
.map(ext => new File([], `test.${ext}`))
.map(f => toFileWithPath(f));
.map(f => withMimeType(f, COMMON_MIME_TYPES));

for (const file of files) {
expect(types.includes(file.type)).toBe(true);
Expand Down
Loading