Skip to content

Commit 3c7a2e1

Browse files
author
Roland Groza
committed
feat!: move full MIME table to file-selector/mime subpath to shrink bundle
BREAKING CHANGE: the full extension-to-MIME table is no longer bundled into the core entry. Import COMMON_MIME_TYPES from file-selector/mime and pass it via the mimeTypes option to restore full coverage.
1 parent 1377f67 commit 3c7a2e1

12 files changed

Lines changed: 1451 additions & 1221 deletions

File tree

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ jobs:
6969
with:
7070
file: ./coverage/lcov.info
7171

72+
size:
73+
name: Check bundle size
74+
runs-on: ubuntu-latest
75+
76+
steps:
77+
- name: Checkout repository
78+
uses: actions/checkout@v7
79+
80+
- name: Setup Node.js
81+
uses: actions/setup-node@v6
82+
with:
83+
node-version: 24
84+
85+
- name: Install dependencies
86+
run: npm ci
87+
88+
# Runs size-limit (builds first via the `presize` script), failing if the main entry exceeds
89+
# its budget — e.g. if the full MIME table leaks back out of the `file-selector/mime` subpath.
90+
- name: Check bundle size
91+
run: npm run size
92+
7293
e2e:
7394
name: Run e2e (browser) tests
7495
runs-on: ubuntu-latest

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ document.addEventListener('drop', async evt => {
8484
});
8585
```
8686

87+
### MIME types
88+
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.
89+
90+
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:
91+
```ts
92+
import {fromEvent} from 'file-selector';
93+
import {COMMON_MIME_TYPES} from 'file-selector/mime';
94+
95+
const files = await fromEvent(evt, {mimeTypes: COMMON_MIME_TYPES});
96+
```
97+
98+
You can also pass your own `Map<extension, mimeType>` to restrict or extend the lookup:
99+
```ts
100+
const files = await fromEvent(evt, {mimeTypes: new Map([['dwg', 'image/vnd.dwg']])});
101+
```
102+
87103

88104
## Browser Support
89105
Most browser support basic File selection with drag 'n' drop or file input:

package-lock.json

Lines changed: 76 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"import": "./dist/index.js",
1313
"require": "./dist/index.cjs"
1414
},
15+
"./mime": {
16+
"types": "./dist/mime.d.ts",
17+
"import": "./dist/mime.js",
18+
"require": "./dist/mime.cjs"
19+
},
1520
"./package.json": "./package.json"
1621
},
1722
"sideEffects": false,
@@ -55,21 +60,37 @@
5560
"test": "vitest",
5661
"test:cov": "vitest run --coverage",
5762
"pretest:e2e": "npm run build",
58-
"test:e2e": "playwright test"
63+
"test:e2e": "playwright test",
64+
"presize": "npm run build",
65+
"size": "size-limit"
5966
},
6067
"devDependencies": {
6168
"@playwright/test": "^1.61.1",
69+
"@size-limit/file": "^12.1.0",
6270
"@types/node": "^26.1.1",
6371
"@vitest/coverage-v8": "^4.1.10",
6472
"jsdom": "^29.1.1",
6573
"oxfmt": "^0.58.0",
6674
"oxlint": "^1.73.0",
6775
"oxlint-tsgolint": "^0.24.0",
76+
"size-limit": "^12.1.0",
6877
"tsdown": "^0.22.4",
6978
"typescript": "^7.0.2",
7079
"vitest": "^4.1.10"
7180
},
7281
"engines": {
7382
"node": ">= 20"
74-
}
83+
},
84+
"size-limit": [
85+
{
86+
"name": "core, esm (fromEvent + default MIME map)",
87+
"path": ["dist/index.js", "dist/mime-default.js"],
88+
"limit": "3 kB"
89+
},
90+
{
91+
"name": "core, cjs (fromEvent + default MIME map)",
92+
"path": ["dist/index.cjs", "dist/mime-default.cjs"],
93+
"limit": "4 kB"
94+
}
95+
]
7596
}

src/file-selector.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,38 @@ it('should fallback to getAsFile when getAsFileSystemHandle resolves to undefine
427427
expect(file.path).toBe(`./${name}`);
428428
});
429429

430+
it('guesses the {type} from the extension using the built-in defaults', async () => {
431+
const mockFile = createFile('test.png', {ping: true}); // typeless File
432+
const evt = inputEvtFromFiles(mockFile);
433+
434+
const [file] = (await fromEvent(evt)) as FileWithPath[];
435+
expect(file.type).toBe('image/png');
436+
});
437+
438+
it('does not guess the {type} for extensions outside the built-in defaults', async () => {
439+
const mockFile = createFile('test.3mf', {ping: true}); // uncommon, not in DEFAULT_MIME_TYPES
440+
const evt = inputEvtFromFiles(mockFile);
441+
442+
const [file] = (await fromEvent(evt)) as FileWithPath[];
443+
expect(file.type).toBe('');
444+
});
445+
446+
it('guesses the {type} from a custom {mimeTypes} lookup when provided', async () => {
447+
const mockFile = createFile('test.3mf', {ping: true}); // typeless File
448+
const evt = inputEvtFromFiles(mockFile);
449+
450+
const [file] = (await fromEvent(evt, {mimeTypes: new Map([['3mf', 'model/3mf']])})) as FileWithPath[];
451+
expect(file.type).toBe('model/3mf');
452+
});
453+
454+
it('does not overwrite a {type} already set by the browser', async () => {
455+
const mockFile = createFile('test.png', {ping: true}, {type: 'application/octet-stream'});
456+
const evt = inputEvtFromFiles(mockFile);
457+
458+
const [file] = (await fromEvent(evt)) as FileWithPath[];
459+
expect(file.type).toBe('application/octet-stream');
460+
});
461+
430462
function dragEvtFromItems(items: DataTransferItem | DataTransferItem[], type: string = 'drop'): DragEvent {
431463
return {
432464
type,

src/file-selector.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import {UnexpectedObjectError} from './error';
2-
import {type FileWithPath, toFileWithPath} from './file';
2+
import {type FileWithPath, toFileWithPath, withMimeType} from './file';
3+
import {DEFAULT_MIME_TYPES} from './mime-default';
34

45
const FILES_TO_IGNORE = [
56
// Thumbnail cache files for macOS and Windows
67
'.DS_Store', // macOs
78
'Thumbs.db' // Windows
89
];
910

11+
export interface FromEventOptions {
12+
/**
13+
* Extension-to-MIME lookup used to set `type` on files the browser left typeless.
14+
* Defaults to a small built-in set of common types ({@link DEFAULT_MIME_TYPES}).
15+
*
16+
* The full extension-to-MIME table (~1,200 entries) is not bundled into the main entry;
17+
* import it from the `file-selector/mime` subpath when broader coverage is needed:
18+
*
19+
* ```ts
20+
* import {fromEvent} from 'file-selector';
21+
* import {COMMON_MIME_TYPES} from 'file-selector/mime';
22+
* await fromEvent(evt, {mimeTypes: COMMON_MIME_TYPES});
23+
* ```
24+
*
25+
* See https://github.com/react-dropzone/file-selector/issues/127
26+
*/
27+
mimeTypes?: Map<string, string>;
28+
}
29+
1030
/**
1131
* Convert a DragEvent's DataTrasfer object to a list of File objects
1232
* NOTE: If some of the items are folders,
@@ -16,8 +36,19 @@ const FILES_TO_IGNORE = [
1636
* and a list of File objects will be returned.
1737
*
1838
* @param evt
39+
* @param options
1940
*/
20-
export async function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
41+
export async function fromEvent(
42+
evt: Event | any,
43+
{mimeTypes = DEFAULT_MIME_TYPES}: FromEventOptions = {}
44+
): Promise<(FileWithPath | DataTransferItem)[]> {
45+
const items = await getFilesOrItems(evt);
46+
// Guess a MIME type from the file extension once, over the final flat list, for any file the
47+
// browser left typeless. DataTransferItems (returned for non-'drop' drag events) pass through as-is.
48+
return items.map(item => (item instanceof File ? withMimeType(item, mimeTypes) : item));
49+
}
50+
51+
async function getFilesOrItems(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
2152
if (isObject<DragEvent>(evt) && isDataTransfer(evt.dataTransfer)) {
2253
return getDataTransferFiles(evt.dataTransfer, evt.type);
2354
} else if (isChangeEvt(evt)) {

src/file.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {COMMON_MIME_TYPES, toFileWithPath} from './file';
1+
import {toFileWithPath, withMimeType} from './file';
2+
import {COMMON_MIME_TYPES} from './mime';
3+
import {DEFAULT_MIME_TYPES} from './mime-default';
24

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

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

124126
test('{type} is enumerable', () => {
125127
const file = new File([], 'test.gif');
126-
const fileWithPath = toFileWithPath(file);
128+
const fileWithPath = withMimeType(toFileWithPath(file), DEFAULT_MIME_TYPES);
127129

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

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

145147
for (const file of files) {
146148
expect(types.includes(file.type)).toBe(true);

0 commit comments

Comments
 (0)