From 9d0cfaf26799a8f38a50a564e431d055562e1862 Mon Sep 17 00:00:00 2001 From: Borewit Date: Sat, 6 Jul 2024 16:21:56 +0200 Subject: [PATCH] Move Node.js dependencies from `core.js` to `index.js` Introduce class `NodeFileTypeParser`, overriding `fromStream()`, which also allow a Node.js Readable stream. --- core.d.ts | 11 +++++------ core.js | 3 +-- index.d.ts | 13 +++++++++++++ index.js | 18 +++++++++++++++++- index.test-d.ts | 6 +++--- test.js | 34 +++++++++++++++++----------------- 6 files changed, 56 insertions(+), 29 deletions(-) diff --git a/core.d.ts b/core.d.ts index c0bb1b99..dd7177e9 100644 --- a/core.d.ts +++ b/core.d.ts @@ -2,7 +2,6 @@ * Typings for primary entry point, Node.js specific typings can be found in index.d.ts */ -import type {Readable as NodeReadableStream} from 'node:stream'; import type {ReadableStream as WebReadableStream} from 'node:stream/web'; import type {ITokenizer} from 'strtok3'; @@ -323,7 +322,7 @@ export type FileTypeResult = { readonly mime: MimeType; }; -export type ReadableStreamWithFileType = NodeReadableStream & { +export type ReadableStreamWithFileType = WebReadableStream & { readonly fileType?: FileTypeResult; }; @@ -347,7 +346,7 @@ The file type is detected by checking the [magic number](https://en.wikipedia.or @param stream - A Node.js Readable stream or Web API Readable Stream representing file data. The Web Readable stream **must be a byte stream**. @returns The detected file type, or `undefined` when there is no match. */ -export function fileTypeFromStream(stream: NodeReadableStream | WebReadableStream): Promise; +export function fileTypeFromStream(stream: WebReadableStream): Promise; /** Detect the file type from an [`ITokenizer`](https://github.com/Borewit/strtok3#tokenizer) source. @@ -425,7 +424,7 @@ if (stream2.fileType?.mime === 'image/jpeg') { } ``` */ -export function fileTypeStream(readableStream: NodeReadableStream, options?: StreamOptions): Promise; +export function fileTypeStream(readableStream: WebReadableStream, options?: StreamOptions): Promise; /** Detect the file type of a [`Blob`](https://nodejs.org/api/buffer.html#class-blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). @@ -516,7 +515,7 @@ export declare class FileTypeParser { /** Works the same way as {@link fileTypeFromStream}, additionally taking into account custom detectors (if any were provided to the constructor). */ - fromStream(stream: NodeReadableStream | WebReadableStream): Promise; + fromStream(stream: WebReadableStream): Promise; /** Works the same way as {@link fileTypeFromTokenizer}, additionally taking into account custom detectors (if any were provided to the constructor). @@ -531,5 +530,5 @@ export declare class FileTypeParser { /** Works the same way as {@link fileTypeStream}, additionally taking into account custom detectors (if any were provided to the constructor). */ - toDetectionStream(readableStream: NodeReadableStream, options?: StreamOptions): Promise; + toDetectionStream(readableStream: WebReadableStream, options?: StreamOptions): Promise; } diff --git a/core.js b/core.js index 7b5fed16..33e23f18 100644 --- a/core.js +++ b/core.js @@ -2,7 +2,6 @@ * Primary entry point, Node.js specific entry point is index.js */ -import {ReadableStream as WebReadableStream} from 'node:stream/web'; import * as Token from 'token-types'; import * as strtok3 from 'strtok3/core'; import {includes, indexOf, getUintBE} from 'uint8array-extras'; @@ -97,7 +96,7 @@ export class FileTypeParser { } async fromStream(stream) { - const tokenizer = await (stream instanceof WebReadableStream ? strtok3.fromWebStream(stream) : strtok3.fromStream(stream)); + const tokenizer = await strtok3.fromWebStream(stream); try { return await this.fromTokenizer(tokenizer); } finally { diff --git a/index.d.ts b/index.d.ts index 799eaa65..e97378a2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,7 +2,18 @@ * Typings for Node.js specific entry point */ +import type {Readable as NodeReadableStream} from 'node:stream'; +import type {ReadableStream as WebReadableStream} from 'node:stream/web'; import type {FileTypeResult} from './core.js'; +import {FileTypeParser} from './core.js'; + +export declare class NodeFileTypeParser extends FileTypeParser { + /** + * + * @param stream Node.js Stream readable or Web API StreamReadable + */ + fromStream(stream: WebReadableStream): Promise; +} /** * Detect the file type of a file path. @@ -14,4 +25,6 @@ import type {FileTypeResult} from './core.js'; */ export function fileTypeFromFile(path: string): Promise; +export function fileTypeFromStream(stream: WebReadableStream | NodeReadableStream): Promise; + export * from './core.js'; diff --git a/index.js b/index.js index 34f8a6b1..4d582101 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,21 @@ * Node.js specific entry point */ +import {ReadableStream as WebReadableStream} from 'node:stream/web'; import * as strtok3 from 'strtok3'; import {FileTypeParser} from './core.js'; +export class NodeFileTypeParser extends FileTypeParser { + async fromStream(stream) { + const tokenizer = await (stream instanceof WebReadableStream ? strtok3.fromWebStream(stream) : strtok3.fromStream(stream)); + try { + return super.fromTokenizer(tokenizer); + } finally { + await tokenizer.close(); + } + } +} + export async function fileTypeFromFile(path, fileTypeOptions) { const tokenizer = await strtok3.fromFile(path); try { @@ -15,4 +27,8 @@ export async function fileTypeFromFile(path, fileTypeOptions) { } } -export * from './core.js'; +export async function fileTypeFromStream(stream, fileTypeOptions) { + return (new NodeFileTypeParser(fileTypeOptions)).fromStream(stream); +} + +export {fileTypeFromBuffer, fileTypeFromBlob, fileTypeStream, FileTypeParser, supportedMimeTypes, supportedExtensions} from './core.js'; diff --git a/index.test-d.ts b/index.test-d.ts index 4518db65..717113bc 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -54,8 +54,8 @@ expectType>(supportedExtensions); expectType>(supportedMimeTypes); -const readableStream = createReadStream('file.png'); -const streamWithFileType = fileTypeStream(readableStream); +const emptyBlob = new Blob([]); +const streamWithFileType = fileTypeStream(emptyBlob.stream()); expectType>(streamWithFileType); (async () => { const {fileType} = await streamWithFileType; @@ -63,4 +63,4 @@ expectType>(streamWithFileType); })(); // Browser -expectType>(fileTypeFromBlob(new Blob())); +expectType>(fileTypeFromBlob(new Blob([]))); diff --git a/test.js b/test.js index 3fb92a10..fab062a7 100644 --- a/test.js +++ b/test.js @@ -10,13 +10,13 @@ import * as strtok3 from 'strtok3/core'; import {areUint8ArraysEqual} from 'uint8array-extras'; import { fileTypeFromBuffer, - fileTypeFromStream, + fileTypeFromStream as fileTypeNodeFromStream, fileTypeFromFile, fileTypeFromBlob, - FileTypeParser, fileTypeStream, supportedExtensions, supportedMimeTypes, + NodeFileTypeParser, } from './index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -323,10 +323,10 @@ async function testFalsePositive(t, ext, name) { t.is(await fileTypeFromBuffer(chunk.buffer), undefined); } -async function testFileFromStream(t, ext, name) { +async function testFileNodeFromStream(t, ext, name) { const filename = `${(name ?? 'fixture')}.${ext}`; const file = path.join(__dirname, 'fixture', filename); - const fileType = await fileTypeFromStream(fs.createReadStream(file)); + const fileType = await fileTypeNodeFromStream(fs.createReadStream(file)); t.truthy(fileType, `identify ${filename}`); t.is(fileType.ext, ext, 'fileType.ext'); @@ -383,7 +383,7 @@ for (const type of types) { _test(`${name}.${type} ${i++} .fileTypeFromBlob() method - same fileType`, testFromBlob, type, name); } - _test(`${name}.${type} ${i++} .fileTypeFromStream() method - same fileType`, testFileFromStream, type, name); + _test(`${name}.${type} ${i++} .fileTypeFromStream() Node.js method - same fileType`, testFileNodeFromStream, type, name); test(`${name}.${type} ${i++} .fileTypeStream() - identical streams`, testStream, type, name); } } else { @@ -392,7 +392,7 @@ for (const type of types) { _test(`${type} ${i++} .fileTypeFromFile()`, testFromFile, type); _test(`${type} ${i++} .fileTypeFromBuffer()`, testFromBuffer, type); - _test(`${type} ${i++} .fileTypeFromStream()`, testFileFromStream, type); + _test(`${type} ${i++} .fileTypeFromStream() Node.js`, testFileNodeFromStream, type); test(`${type} ${i++} .fileTypeStream() - identical streams`, testStream, type); } @@ -647,7 +647,7 @@ test('odd file sizes', async t => { for (const size of oddFileSizes) { const buffer = new Uint8Array(size); const stream = new BufferedStream(buffer); - await t.notThrowsAsync(fileTypeFromStream(stream), `fromStream: File size: ${size} bytes`); + await t.notThrowsAsync(fileTypeNodeFromStream(stream), `fromStream: File size: ${size} bytes`); } }); @@ -713,7 +713,7 @@ if (nodeMajorVersion >= nodeVersionSupportingByeBlobStream) { const blob = new Blob([header]); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBlob(blob); t.deepEqual(result, {ext: 'unicorn', mime: 'application/unicorn'}); @@ -725,7 +725,7 @@ if (nodeMajorVersion >= nodeVersionSupportingByeBlobStream) { const blob = new Blob([chunk]); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBlob(blob); t.deepEqual(result, {ext: 'png', mime: 'image/png'}); @@ -737,7 +737,7 @@ if (nodeMajorVersion >= nodeVersionSupportingByeBlobStream) { const blob = new Blob([chunk]); const customDetectors = [mockPngDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBlob(blob); t.deepEqual(result, {ext: 'mockPng', mime: 'image/mockPng'}); @@ -749,7 +749,7 @@ test('fileTypeFromBuffer should detect custom file type "unicorn" using custom d const uint8ArrayContent = new TextEncoder().encode(header); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBuffer(uint8ArrayContent); t.deepEqual(result, {ext: 'unicorn', mime: 'application/unicorn'}); @@ -760,7 +760,7 @@ test('fileTypeFromBuffer should keep detecting default file types when no custom const uint8ArrayContent = fs.readFileSync(file); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBuffer(uint8ArrayContent); t.deepEqual(result, {ext: 'png', mime: 'image/png'}); @@ -771,7 +771,7 @@ test('fileTypeFromBuffer should allow overriding default file type detectors', a const uint8ArrayContent = fs.readFileSync(file); const customDetectors = [mockPngDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromBuffer(uint8ArrayContent); t.deepEqual(result, {ext: 'mockPng', mime: 'image/mockPng'}); @@ -786,7 +786,7 @@ test('fileTypeFromStream should detect custom file type "unicorn" using custom d const readableStream = new CustomReadableStream(); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromStream(readableStream); t.deepEqual(result, {ext: 'unicorn', mime: 'application/unicorn'}); @@ -797,7 +797,7 @@ test('fileTypeFromStream should keep detecting default file types when no custom const readableStream = fs.createReadStream(file); const customDetectors = [unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromStream(readableStream); t.deepEqual(result, {ext: 'png', mime: 'image/png'}); @@ -808,7 +808,7 @@ test('fileTypeFromStream should allow overriding default file type detectors', a const readableStream = fs.createReadStream(file); const customDetectors = [mockPngDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromStream(readableStream); t.deepEqual(result, {ext: 'mockPng', mime: 'image/mockPng'}); @@ -847,7 +847,7 @@ test('fileTypeFromTokenizer should return undefined when a custom detector chang // Include the unicormDetector here to verify it's not used after the tokenizer.position changed const customDetectors = [tokenizerPositionChanger, unicornDetector]; - const parser = new FileTypeParser({customDetectors}); + const parser = new NodeFileTypeParser({customDetectors}); const result = await parser.fromTokenizer(strtok3.fromBuffer(uint8ArrayContent)); t.is(result, undefined);