From 4b9f7959e570ee075cbf55caf44a5c7f55a663e0 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Tue, 24 Aug 2021 11:34:46 +0200 Subject: [PATCH] feat: Only export as ESM. --- .github/workflows/ci.yml | 2 +- .gitignore | 15 +-- .npmrc | 1 + README.md | 7 +- dist/cjs/callback.js | 23 ---- dist/cjs/index.js | 59 ----------- dist/cjs/internals.js | 121 ---------------------- dist/cjs/models.js | 20 ---- dist/cjs/stream.js | 48 --------- dist/mjs/callback.mjs | 19 ---- dist/mjs/index.mjs | 54 ---------- dist/mjs/internals.mjs | 112 -------------------- dist/mjs/models.mjs | 16 --- dist/mjs/stream.mjs | 44 -------- package.json | 45 ++++---- prettier.config.js => prettier.config.cjs | 0 src/index.ts | 59 +++++------ src/internals.ts | 75 +++++++++----- src/stream.ts | 7 +- test/buffersAndStreams.test.ts | 12 ++- test/files.test.ts | 13 ++- test/index.test.ts | 2 +- test/streams.test.ts | 10 +- test/urls.test.ts | 7 +- tsconfig.json | 10 +- tsconfig.modules.json | 9 -- types/callback.d.ts | 3 - types/index.d.ts | 6 -- types/internals.d.ts | 8 -- types/models.d.ts | 22 ---- types/stream.d.ts | 16 --- 31 files changed, 142 insertions(+), 703 deletions(-) create mode 100644 .npmrc delete mode 100644 dist/cjs/callback.js delete mode 100644 dist/cjs/index.js delete mode 100644 dist/cjs/internals.js delete mode 100644 dist/cjs/models.js delete mode 100644 dist/cjs/stream.js delete mode 100644 dist/mjs/callback.mjs delete mode 100644 dist/mjs/index.mjs delete mode 100644 dist/mjs/internals.mjs delete mode 100644 dist/mjs/models.mjs delete mode 100644 dist/mjs/stream.mjs rename prettier.config.js => prettier.config.cjs (100%) delete mode 100644 tsconfig.modules.json delete mode 100644 types/callback.d.ts delete mode 100644 types/index.d.ts delete mode 100644 types/internals.d.ts delete mode 100644 types/models.d.ts delete mode 100644 types/stream.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb67dcf..909cedb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - name: Use Node.js LTS uses: actions/setup-node@v2 with: - node-version: 12.x + node-version: lts/* - name: Restore cached dependencies uses: actions/cache@v2 with: diff --git a/.gitignore b/.gitignore index 1d63b1c..8e36349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,3 @@ -package-lock.json -yarn.lock - -.nyc_output -coverage/ -node_modules/ -tmp/ - -npm-debug.log -npm-error.log -yarn-debug.log -yarn-error.log \ No newline at end of file +dist/ +types/ +coverage/ \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..bf2e764 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/README.md b/README.md index 3c6ae43..f0428f9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ If `callback` is not provided, the method returns a `Promise`. ## Example ```js -const { info } = require('fastimage') +import { info } from 'fastimage' info('http://fakeimg.pl/1000x1000/', (error, data) => { if (error) { @@ -49,7 +49,7 @@ info('http://fakeimg.pl/1000x1000/', (error, data) => { } }) -const data = await fastimage('http://fakeimg.pl/1000x1000/') +const data = await info('http://fakeimg.pl/1000x1000/') ``` The callback argument (or the resolved value) will be an object with the following properties: @@ -73,7 +73,8 @@ Calling `fastimage.stream` it will return a Writable stream which will emit the The stream accepts only the `threshold` option. ```js -const { info } = require('fastimage') +import { stream } from 'fastimage' + const pipe = createReadStream('/path/to/image.png').pipe(stream({ threshold: 100 })) pipe.on('info', data => { diff --git a/dist/cjs/callback.js b/dist/cjs/callback.js deleted file mode 100644 index 199a601..0000000 --- a/dist/cjs/callback.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ensurePromiseCallback = void 0; -function ensurePromiseCallback(callback) { - if (typeof callback === 'function') { - return [callback]; - } - let promiseResolve, promiseReject; - const promise = new Promise((resolve, reject) => { - promiseResolve = resolve; - promiseReject = reject; - }); - return [ - (err, info) => { - if (err) { - return promiseReject(err); - } - return promiseResolve(info); - }, - promise - ]; -} -exports.ensurePromiseCallback = ensurePromiseCallback; diff --git a/dist/cjs/index.js b/dist/cjs/index.js deleted file mode 100644 index 7aac196..0000000 --- a/dist/cjs/index.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.stream = exports.info = void 0; -const callback_1 = require("./callback"); -const internals_1 = require("./internals"); -const models_1 = require("./models"); -const stream_1 = require("./stream"); -function info(source, options, cb) { - // Normalize arguments - if (typeof options === 'function') { - cb = options; - options = {}; - } - const { timeout, threshold, userAgent } = { ...models_1.defaultOptions, ...options }; - // Prepare execution - let finished = false; - let response; - let buffer = Buffer.alloc(0); - const [callback, promise] = callback_1.ensurePromiseCallback(cb); - const start = process.hrtime.bigint(); - // Make sure the source is always a Stream - let stream; - let url; - try { - ; - [stream, url] = internals_1.toStream(source, timeout, threshold, userAgent); - } - catch (e) { - callback(e); - return promise; - } - // When dealing with URLs, save the response to extract data later - stream.on('response', (r) => { - response = r; - }); - stream.on('data', (chunk) => { - if (finished) { - return; - } - buffer = Buffer.concat([buffer, chunk]); - finished = internals_1.handleData(buffer, response, threshold, start, callback); - }); - stream.on('error', (error) => { - callback(internals_1.handleError(error, url)); - }); - stream.on('end', () => { - if (finished) { - return; - } - // We have reached the end without figuring the image type. Just give up - callback(new models_1.FastImageError('Unsupported data.', 'UNSUPPORTED')); - }); - return promise; -} -exports.info = info; -function stream(options) { - return new stream_1.FastImageStream(options !== null && options !== void 0 ? options : {}); -} -exports.stream = stream; diff --git a/dist/cjs/internals.js b/dist/cjs/internals.js deleted file mode 100644 index ab90814..0000000 --- a/dist/cjs/internals.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.handleError = exports.handleData = exports.toStream = void 0; -const fs_1 = require("fs"); -const got_1 = __importDefault(require("got")); -const image_size_1 = __importDefault(require("image-size")); -const stream_1 = require("stream"); -const models_1 = require("./models"); -function toStream(source, timeout, threshold, userAgent) { - let url; - const highWaterMark = threshold > 0 ? Math.floor(threshold / 10) : 1024; - // If the source is a buffer, get it as stream - if (Buffer.isBuffer(source)) { - source = stream_1.Readable.from(source, { highWaterMark }); - } - else if (typeof source === 'string') { - // Try to parse the source as URL - If it succeeds, we will fetch it - try { - const parsedUrl = new URL(source); - if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { - throw new models_1.FastImageError('Invalid URL.', 'URL_ERROR', parsedUrl.toString()); - } - url = source; - source = got_1.default.stream(parsedUrl.toString(), { - headers: { 'user-agent': userAgent }, - followRedirect: true, - timeout - }); - } - catch (e) { - if (e.code === 'FASTIMAGE_URL_ERROR') { - throw e; - } - // Parsing failed. Treat as local file - source = fs_1.createReadStream(source, { highWaterMark }); - } - } - return [source, url]; -} -exports.toStream = toStream; -function handleData(buffer, response, threshold, start, callback) { - try { - const info = image_size_1.default(buffer); - const data = { - width: info.width, - height: info.height, - type: info.type, - time: Number(process.hrtime.bigint() - start) / 1e6, - analyzed: buffer.length - }; - // Add URL informations - if (response) { - data.realUrl = response.url; - /* istanbul ignore else */ - if ('content-length' in response.headers) { - data.size = parseInt(response.headers['content-length'], 10); - } - } - // Close the URL if possible - if (response) { - response.destroy(); - } - callback(null, data); - return true; - } - catch (e) { - // Check threshold - if (threshold > 0 && buffer.length > threshold) { - if (response) { - response.destroy(); - } - callback(new models_1.FastImageError('Unsupported data.', 'UNSUPPORTED')); - return true; - } - return false; - } -} -exports.handleData = handleData; -function handleError(error, url) { - let message = null; - let code = 'NETWORK_ERROR'; - switch (error.code) { - case 'EISDIR': - code = 'FS_ERROR'; - message = 'Source is a directory.'; - break; - case 'ENOENT': - code = 'FS_ERROR'; - message = 'Source not found.'; - break; - case 'EACCES': - code = 'FS_ERROR'; - message = 'Source is not readable.'; - break; - case 'ENOTFOUND': - message = 'Invalid remote host requested.'; - break; - case 'ECONNRESET': - case 'EPIPE': - message = 'Connection with the remote host interrupted.'; - break; - case 'ECONNREFUSED': - message = 'Connection refused from the remote host.'; - break; - case 'ETIMEDOUT': - message = 'Connection to the remote host timed out.'; - break; - } - if (error.response) { - message = `Remote host replied with HTTP ${error.response.statusCode}.`; - } - /* istanbul ignore else */ - if (message) { - error = new models_1.FastImageError(message, code, url); - } - return error; -} -exports.handleError = handleError; diff --git a/dist/cjs/models.js b/dist/cjs/models.js deleted file mode 100644 index 9c2d859..0000000 --- a/dist/cjs/models.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -// The version is dynamically generated via build script in order not rely on require in the ESM case. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultOptions = exports.userAgentVersion = exports.FastImageError = void 0; -class FastImageError extends Error { - constructor(message, code, url, httpResponseCode) { - super(message); - this.code = `FASTIMAGE_${code}`; - this.url = url; - this.httpResponseCode = httpResponseCode; - } -} -exports.FastImageError = FastImageError; -// Since it's harder to keep this in sync with package.json, let's use a different number. -exports.userAgentVersion = '1.0.0'; -exports.defaultOptions = { - timeout: 30000, - threshold: 4096, - userAgent: `fastimage/${exports.userAgentVersion}` -}; diff --git a/dist/cjs/stream.js b/dist/cjs/stream.js deleted file mode 100644 index d3afa5e..0000000 --- a/dist/cjs/stream.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FastImageStream = void 0; -const stream_1 = require("stream"); -const internals_1 = require("./internals"); -const models_1 = require("./models"); -class FastImageStream extends stream_1.Writable { - constructor(options) { - var _a; - super(options); - this.threshold = (_a = options.threshold) !== null && _a !== void 0 ? _a : models_1.defaultOptions.threshold; - this.buffer = Buffer.alloc(0); - this.start = process.hrtime.bigint(); - this.finished = false; - } - analyze(chunk) { - this.buffer = Buffer.concat([this.buffer, chunk]); - this.finished = internals_1.handleData(this.buffer, undefined, this.threshold, this.start, (error, data) => { - if (error) { - this.emit('error', error); - } - else { - this.emit('info', data); - } - this.destroy(); - }); - } - _write(chunk, _e, cb) { - this.analyze(chunk); - cb(); - } - /* istanbul ignore next */ - _writev(chunks, cb) { - for (const { chunk } of chunks) { - this.analyze(chunk); - } - cb(); - } - _final(cb) { - /* istanbul ignore if */ - if (this.finished) { - cb(); - return; - } - cb(new models_1.FastImageError('Unsupported data.', 'UNSUPPORTED')); - } -} -exports.FastImageStream = FastImageStream; diff --git a/dist/mjs/callback.mjs b/dist/mjs/callback.mjs deleted file mode 100644 index b66723b..0000000 --- a/dist/mjs/callback.mjs +++ /dev/null @@ -1,19 +0,0 @@ -export function ensurePromiseCallback(callback) { - if (typeof callback === 'function') { - return [callback]; - } - let promiseResolve, promiseReject; - const promise = new Promise((resolve, reject) => { - promiseResolve = resolve; - promiseReject = reject; - }); - return [ - (err, info) => { - if (err) { - return promiseReject(err); - } - return promiseResolve(info); - }, - promise - ]; -} diff --git a/dist/mjs/index.mjs b/dist/mjs/index.mjs deleted file mode 100644 index ad33601..0000000 --- a/dist/mjs/index.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { ensurePromiseCallback } from "./callback.mjs"; -import { handleData, handleError, toStream } from "./internals.mjs"; -import { defaultOptions, FastImageError } from "./models.mjs"; -import { FastImageStream } from "./stream.mjs"; -export function info(source, options, cb) { - // Normalize arguments - if (typeof options === 'function') { - cb = options; - options = {}; - } - const { timeout, threshold, userAgent } = { ...defaultOptions, ...options }; - // Prepare execution - let finished = false; - let response; - let buffer = Buffer.alloc(0); - const [callback, promise] = ensurePromiseCallback(cb); - const start = process.hrtime.bigint(); - // Make sure the source is always a Stream - let stream; - let url; - try { - ; - [stream, url] = toStream(source, timeout, threshold, userAgent); - } - catch (e) { - callback(e); - return promise; - } - // When dealing with URLs, save the response to extract data later - stream.on('response', (r) => { - response = r; - }); - stream.on('data', (chunk) => { - if (finished) { - return; - } - buffer = Buffer.concat([buffer, chunk]); - finished = handleData(buffer, response, threshold, start, callback); - }); - stream.on('error', (error) => { - callback(handleError(error, url)); - }); - stream.on('end', () => { - if (finished) { - return; - } - // We have reached the end without figuring the image type. Just give up - callback(new FastImageError('Unsupported data.', 'UNSUPPORTED')); - }); - return promise; -} -export function stream(options) { - return new FastImageStream(options !== null && options !== void 0 ? options : {}); -} diff --git a/dist/mjs/internals.mjs b/dist/mjs/internals.mjs deleted file mode 100644 index f510799..0000000 --- a/dist/mjs/internals.mjs +++ /dev/null @@ -1,112 +0,0 @@ -import { createReadStream } from 'fs'; -import got from 'got'; -import imageSize from 'image-size'; -import { Readable } from 'stream'; -import { FastImageError } from "./models.mjs"; -export function toStream(source, timeout, threshold, userAgent) { - let url; - const highWaterMark = threshold > 0 ? Math.floor(threshold / 10) : 1024; - // If the source is a buffer, get it as stream - if (Buffer.isBuffer(source)) { - source = Readable.from(source, { highWaterMark }); - } - else if (typeof source === 'string') { - // Try to parse the source as URL - If it succeeds, we will fetch it - try { - const parsedUrl = new URL(source); - if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { - throw new FastImageError('Invalid URL.', 'URL_ERROR', parsedUrl.toString()); - } - url = source; - source = got.stream(parsedUrl.toString(), { - headers: { 'user-agent': userAgent }, - followRedirect: true, - timeout - }); - } - catch (e) { - if (e.code === 'FASTIMAGE_URL_ERROR') { - throw e; - } - // Parsing failed. Treat as local file - source = createReadStream(source, { highWaterMark }); - } - } - return [source, url]; -} -export function handleData(buffer, response, threshold, start, callback) { - try { - const info = imageSize(buffer); - const data = { - width: info.width, - height: info.height, - type: info.type, - time: Number(process.hrtime.bigint() - start) / 1e6, - analyzed: buffer.length - }; - // Add URL informations - if (response) { - data.realUrl = response.url; - /* istanbul ignore else */ - if ('content-length' in response.headers) { - data.size = parseInt(response.headers['content-length'], 10); - } - } - // Close the URL if possible - if (response) { - response.destroy(); - } - callback(null, data); - return true; - } - catch (e) { - // Check threshold - if (threshold > 0 && buffer.length > threshold) { - if (response) { - response.destroy(); - } - callback(new FastImageError('Unsupported data.', 'UNSUPPORTED')); - return true; - } - return false; - } -} -export function handleError(error, url) { - let message = null; - let code = 'NETWORK_ERROR'; - switch (error.code) { - case 'EISDIR': - code = 'FS_ERROR'; - message = 'Source is a directory.'; - break; - case 'ENOENT': - code = 'FS_ERROR'; - message = 'Source not found.'; - break; - case 'EACCES': - code = 'FS_ERROR'; - message = 'Source is not readable.'; - break; - case 'ENOTFOUND': - message = 'Invalid remote host requested.'; - break; - case 'ECONNRESET': - case 'EPIPE': - message = 'Connection with the remote host interrupted.'; - break; - case 'ECONNREFUSED': - message = 'Connection refused from the remote host.'; - break; - case 'ETIMEDOUT': - message = 'Connection to the remote host timed out.'; - break; - } - if (error.response) { - message = `Remote host replied with HTTP ${error.response.statusCode}.`; - } - /* istanbul ignore else */ - if (message) { - error = new FastImageError(message, code, url); - } - return error; -} diff --git a/dist/mjs/models.mjs b/dist/mjs/models.mjs deleted file mode 100644 index 46198f0..0000000 --- a/dist/mjs/models.mjs +++ /dev/null @@ -1,16 +0,0 @@ -// The version is dynamically generated via build script in order not rely on require in the ESM case. -export class FastImageError extends Error { - constructor(message, code, url, httpResponseCode) { - super(message); - this.code = `FASTIMAGE_${code}`; - this.url = url; - this.httpResponseCode = httpResponseCode; - } -} -// Since it's harder to keep this in sync with package.json, let's use a different number. -export const userAgentVersion = '1.0.0'; -export const defaultOptions = { - timeout: 30000, - threshold: 4096, - userAgent: `fastimage/${userAgentVersion}` -}; diff --git a/dist/mjs/stream.mjs b/dist/mjs/stream.mjs deleted file mode 100644 index 951ce29..0000000 --- a/dist/mjs/stream.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { Writable } from 'stream'; -import { handleData } from "./internals.mjs"; -import { defaultOptions, FastImageError } from "./models.mjs"; -export class FastImageStream extends Writable { - constructor(options) { - var _a; - super(options); - this.threshold = (_a = options.threshold) !== null && _a !== void 0 ? _a : defaultOptions.threshold; - this.buffer = Buffer.alloc(0); - this.start = process.hrtime.bigint(); - this.finished = false; - } - analyze(chunk) { - this.buffer = Buffer.concat([this.buffer, chunk]); - this.finished = handleData(this.buffer, undefined, this.threshold, this.start, (error, data) => { - if (error) { - this.emit('error', error); - } - else { - this.emit('info', data); - } - this.destroy(); - }); - } - _write(chunk, _e, cb) { - this.analyze(chunk); - cb(); - } - /* istanbul ignore next */ - _writev(chunks, cb) { - for (const { chunk } of chunks) { - this.analyze(chunk); - } - cb(); - } - _final(cb) { - /* istanbul ignore if */ - if (this.finished) { - cb(); - return; - } - cb(new FastImageError('Unsupported data.', 'UNSUPPORTED')); - } -} diff --git a/package.json b/package.json index e2668e1..5fb01e9 100644 --- a/package.json +++ b/package.json @@ -35,37 +35,38 @@ "LICENSE.md", "README.md" ], - "main": "dist/cjs/index.js", - "exports": { - "require": "./dist/cjs/index.js", - "import": "./dist/mjs/index.mjs" - }, + "type": "module", + "main": "./dist/index.js", + "exports": "./dist/index.js", "typings": "types/index.d.ts", "types": "types/index.d.ts", "scripts": { - "lint": "eslint src/*.ts test/*.ts", - "test": "tap --reporter=spec --coverage-report=html --coverage-report=text --no-browser test/*.test.ts", - "test:ci": "tap --no-color --reporter=spec --coverage-report=json --coverage-report=text --branches 90 --functions 90 --lines 90 --statements 90 test/*.test.ts", - "ci": "yarn lint && yarn test:ci", - "prebuild": "rm -rf dist types && yarn lint", - "build": "tsc -p . && tsc -p tsconfig.modules.json && renamer --find js --replace mjs dist/mjs/* >> /dev/null && jscodeshift -s --extensions=mjs -t node_modules/@cowtech/esm-package-utils dist/mjs/**", - "prepublishOnly": "yarn ci", + "prebuild": "rm -rf dist types && npm run lint", + "build": "tsc -p . && esm-pkg-add-imports-extensions dist", + "format": "prettier -w src test", + "lint": "eslint src test", + "test": "c8 --reporter=text --reporter=html esm-ts-tap --reporter=spec --no-coverage test/*.test.ts", + "test:ci": "c8 --reporter=text --reporter=json --check-coverage --branches 90 --functions 90 --lines 90 --statements 90 esm-ts-tap --no-color --no-coverage test/*.test.ts", + "ci": "npm run lint && npm run test:ci", + "prepublishOnly": "npm run ci", "postpublish": "git push origin && git push origin -f --tags" }, "dependencies": { - "got": "^11.8.1", - "image-size": "^0.9.3" + "image-size": "^1.0.0", + "undici": "^4.4.7" }, "devDependencies": { - "@cowtech/eslint-config": "^7.14.0", - "@cowtech/esm-package-utils": "^0.2.0", - "@types/node": "^14.14.19", - "@types/tap": "^14.10.1", - "prettier": "^2.2.1", - "tap": "^14.11.0", - "typescript": "^4.1.3" + "@cowtech/eslint-config": "^7.14.5", + "@cowtech/esm-package-utils": "^0.9.0", + "@types/node": "^16.6.1", + "@types/tap": "^15.0.5", + "c8": "^7.8.0", + "prettier": "^2.3.2", + "tap": "^15.0.9", + "ts-node": "^10.2.0", + "typescript": "^4.2.4" }, "engines": { - "node": ">=12.15.0" + "node": ">=14.15.0" } } diff --git a/prettier.config.js b/prettier.config.cjs similarity index 100% rename from prettier.config.js rename to prettier.config.cjs diff --git a/src/index.ts b/src/index.ts index e936881..624c137 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import { HTTPError, Response } from 'got' +import EventEmitter from 'events' import { Stream, Writable, WritableOptions } from 'stream' import { Callback, ensurePromiseCallback } from './callback' import { handleData, handleError, toStream } from './internals' import { defaultOptions, FastImageError, ImageInfo, Options } from './models' import { FastImageStream } from './stream' -export function info( +export async function info( source: string | Stream | Buffer, options?: Partial | Callback, cb?: Callback @@ -20,49 +20,42 @@ export function info( // Prepare execution let finished = false - let response: Response let buffer = Buffer.alloc(0) const [callback, promise] = ensurePromiseCallback(cb) const start = process.hrtime.bigint() // Make sure the source is always a Stream - let stream: Stream - let url: string | undefined try { - ;[stream, url] = toStream(source, timeout, threshold, userAgent) - } catch (e) { - callback(e) - return promise! - } - - // When dealing with URLs, save the response to extract data later - stream!.on('response', (r: Response) => { - response = r - }) + const aborter = new EventEmitter() + const [stream, url, headers] = await toStream(source, timeout, threshold, userAgent, aborter) - stream!.on('data', (chunk: Buffer) => { - if (finished) { - return - } + stream.on('data', (chunk: Buffer) => { + if (finished) { + return + } - buffer = Buffer.concat([buffer, chunk]) - finished = handleData(buffer, response, threshold, start, callback) - }) + buffer = Buffer.concat([buffer, chunk]) + finished = handleData(buffer, headers, threshold, start, aborter, callback) + }) - stream!.on('error', (error: FastImageError | HTTPError) => { - callback(handleError(error, url!)) - }) + stream.on('error', (error: FastImageError) => { + callback(handleError(error, url!)) + }) - stream!.on('end', () => { - if (finished) { - return - } + stream.on('end', () => { + if (finished) { + return + } - // We have reached the end without figuring the image type. Just give up - callback(new FastImageError('Unsupported data.', 'UNSUPPORTED')) - }) + // We have reached the end without figuring the image type. Just give up + callback(new FastImageError('Unsupported data.', 'UNSUPPORTED')) + }) - return promise! + return promise! + } catch (e) { + callback(e) + return promise! + } } export function stream(options?: Partial & Partial): Writable { diff --git a/src/internals.ts b/src/internals.ts index c722034..e8557a5 100644 --- a/src/internals.ts +++ b/src/internals.ts @@ -1,17 +1,23 @@ +import EventEmitter from 'events' import { createReadStream } from 'fs' -import got, { HTTPError, Response } from 'got' +import { IncomingHttpHeaders } from 'http' import imageSize from 'image-size' import { Readable, Stream } from 'stream' +import undici from 'undici' import { Callback } from './callback' import { FastImageError, ImageInfo } from './models' -export function toStream( +const realUrlHeader = 'x-fastimage-real-url' + +export async function toStream( source: string | Stream | Buffer, timeout: number, threshold: number, - userAgent: string -): [Stream, string | undefined] { + userAgent: string, + aborter: EventEmitter +): Promise<[Stream, string | undefined, IncomingHttpHeaders | undefined]> { let url: string | undefined + let headers: IncomingHttpHeaders | undefined const highWaterMark = threshold > 0 ? Math.floor(threshold / 10) : 1024 // If the source is a buffer, get it as stream @@ -27,14 +33,36 @@ export function toStream( } url = source - source = got.stream(parsedUrl.toString(), { + + const { + statusCode, + headers: responseHeaders, + body, + context + } = await undici.request(parsedUrl, { + method: 'GET', headers: { 'user-agent': userAgent }, - followRedirect: true, - timeout + signal: aborter, + dispatcher: new undici.Agent({ + headersTimeout: timeout, + bodyTimeout: timeout, + maxRedirections: 10, + pipelining: 0 + }) }) + + if (statusCode > 299) { + throw new FastImageError(`Remote host replied with HTTP ${statusCode}.`, 'NETWORK_ERROR', url) + } + + source = body + headers = responseHeaders + headers[realUrlHeader] = (context as any).history.pop().toString() } catch (e) { if ((e as FastImageError).code === 'FASTIMAGE_URL_ERROR') { throw e + } else if (url) { + throw handleError(e, url) } // Parsing failed. Treat as local file @@ -42,14 +70,15 @@ export function toStream( } } - return [source, url] + return [source, url, headers] } export function handleData( buffer: Buffer, - response: Response | undefined, + headers: IncomingHttpHeaders | undefined, threshold: number, start: bigint, + aborter: EventEmitter, callback: Callback ): boolean { try { @@ -64,28 +93,23 @@ export function handleData( } // Add URL informations - if (response) { - data.realUrl = response.url + if (headers) { + data.realUrl = headers[realUrlHeader] as string - /* istanbul ignore else */ - if ('content-length' in response.headers) { - data.size = parseInt(response.headers['content-length']!, 10) + if ('content-length' in headers) { + data.size = parseInt(headers['content-length']!, 10) } } // Close the URL if possible - if (response) { - response.destroy() - } + aborter.emit('abort') callback(null, data) return true } catch (e) { // Check threshold if (threshold > 0 && buffer.length > threshold) { - if (response) { - response.destroy() - } + aborter.emit('abort') callback(new FastImageError('Unsupported data.', 'UNSUPPORTED')) return true @@ -95,7 +119,7 @@ export function handleData( } } -export function handleError(error: FastImageError | HTTPError, url: string): Error { +export function handleError(error: FastImageError, url: string): Error { let message = null let code = 'NETWORK_ERROR' @@ -115,23 +139,22 @@ export function handleError(error: FastImageError | HTTPError, url: string): Err case 'ENOTFOUND': message = 'Invalid remote host requested.' break + /* c8 ignore next 2 */ case 'ECONNRESET': case 'EPIPE': + case 'UND_ERR_SOCKET': message = 'Connection with the remote host interrupted.' break case 'ECONNREFUSED': message = 'Connection refused from the remote host.' break + /* c8 ignore next */ case 'ETIMEDOUT': + case 'UND_ERR_HEADERS_TIMEOUT': message = 'Connection to the remote host timed out.' break } - if ((error as HTTPError).response) { - message = `Remote host replied with HTTP ${(error as HTTPError).response.statusCode}.` - } - - /* istanbul ignore else */ if (message) { error = new FastImageError(message, code, url) } diff --git a/src/stream.ts b/src/stream.ts index ac7d6cb..f607149 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,3 +1,4 @@ +import EventEmitter from 'events' import { Writable, WritableOptions } from 'stream' import { handleData } from './internals' import { defaultOptions, FastImageError, ImageInfo, Options } from './models' @@ -25,6 +26,7 @@ export class FastImageStream extends Writable { undefined, this.threshold, this.start, + new EventEmitter(), (error: Error | null, data?: ImageInfo) => { if (error) { this.emit('error', error) @@ -42,7 +44,7 @@ export class FastImageStream extends Writable { cb() } - /* istanbul ignore next */ + /* c8 ignore start */ _writev(chunks: Array<{ chunk: any }>, cb: (error?: Error | null) => void): void { for (const { chunk } of chunks) { this.analyze(chunk) @@ -50,9 +52,10 @@ export class FastImageStream extends Writable { cb() } + /* c8 ignore stop */ _final(cb: (error?: Error | null) => void): void { - /* istanbul ignore if */ + /* c8 ignore next 4 */ if (this.finished) { cb() return diff --git a/test/buffersAndStreams.test.ts b/test/buffersAndStreams.test.ts index 90e0b95..55b0c93 100644 --- a/test/buffersAndStreams.test.ts +++ b/test/buffersAndStreams.test.ts @@ -1,17 +1,19 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { createReadStream, readFileSync } from 'fs' -import { resolve } from 'path' import t from 'tap' import { info } from '../src' import { FastImageError } from '../src/models' type Test = typeof t +const fileName = import.meta.url.replace('file://', '') +const imagePath = new URL('fixtures/image.png', import.meta.url).toString().replace('file://', '') + t.test('fastimage.info', (t: Test) => { t.test('when working with buffers', (t: Test) => { t.test('should return the information of a image', async (t: Test) => { - const buffer = readFileSync(resolve(__dirname, 'fixtures/image.png')) + const buffer = readFileSync(imagePath) const data = await info(buffer) @@ -25,7 +27,7 @@ t.test('fastimage.info', (t: Test) => { }) t.test('should return a error when the data is not a image', async (t: Test) => { - const buffer = readFileSync(resolve(__filename)) + const buffer = readFileSync(fileName) await t.rejects(info(buffer), new FastImageError('Unsupported data.', 'UNSUPPORTED')) }) @@ -35,7 +37,7 @@ t.test('fastimage.info', (t: Test) => { t.test('when working with streams', (t: Test) => { t.test('should return the information of a image', async (t: Test) => { - const data = await info(createReadStream(resolve(__dirname, 'fixtures/image.png'))) + const data = await info(createReadStream(imagePath)) t.same(data, { width: 150, @@ -47,7 +49,7 @@ t.test('fastimage.info', (t: Test) => { }) t.test('should return a error when the data is not a image', async (t: Test) => { - await t.rejects(info(createReadStream(__filename)), new FastImageError('Unsupported data.', 'UNSUPPORTED')) + await t.rejects(info(fileName), new FastImageError('Unsupported data.', 'UNSUPPORTED')) }) t.end() diff --git a/test/files.test.ts b/test/files.test.ts index a15c2c6..a516e85 100644 --- a/test/files.test.ts +++ b/test/files.test.ts @@ -1,17 +1,20 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { chmodSync, unlinkSync, writeFileSync } from 'fs' -import { resolve } from 'path' +import { dirname } from 'path' import t from 'tap' import { info } from '../src' import { FastImageError, ImageInfo } from '../src/models' type Test = typeof t +const fileName = import.meta.url.replace('file://', '') +const imagePath = new URL('fixtures/image.png', import.meta.url).toString().replace('file://', '') + t.test('fastimage.info', (t: Test) => { t.test('when working with local files', (t: Test) => { t.test('should return the information of a image', (t: Test) => { - info(resolve(__dirname, 'fixtures/image.png'), (error: Error | null, data?: ImageInfo) => { + info(imagePath, (error: Error | null, data?: ImageInfo) => { t.error(error) t.same(data, { @@ -27,7 +30,7 @@ t.test('fastimage.info', (t: Test) => { }) t.test('should return a error when the path is a directory', (t: Test) => { - info(__dirname, (error: Error | null, data?: ImageInfo) => { + info(dirname(fileName), (error: Error | null, data?: ImageInfo) => { t.error(data) t.strictSame(error, new FastImageError('Source is a directory.', 'FS_ERROR')) t.end() @@ -43,7 +46,7 @@ t.test('fastimage.info', (t: Test) => { }) t.test('should return a error when the path cannot be read', (t: Test) => { - const unreadablePath = resolve(__dirname, './fixtures/unreadable.png') + const unreadablePath = imagePath.replace('image.png', 'unreadable.png') writeFileSync(unreadablePath, 'foo', 'utf-8') chmodSync(unreadablePath, 0) @@ -57,7 +60,7 @@ t.test('fastimage.info', (t: Test) => { }) t.test('should return a error when the path is not a image', (t: Test) => { - info(__filename, (error: Error | null, data?: ImageInfo) => { + info(fileName, (error: Error | null, data?: ImageInfo) => { t.error(data) t.strictSame(error, new FastImageError('Unsupported data.', 'UNSUPPORTED')) t.end() diff --git a/test/index.test.ts b/test/index.test.ts index cef8837..4a5d850 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -32,7 +32,7 @@ t.test('fastimage.info', (t: Test) => { size: 554617 }) - t.true(data.analyzed < data.size!) + t.ok(data.analyzed < data.size!) }) t.end() diff --git a/test/streams.test.ts b/test/streams.test.ts index 4832f72..ee0c37e 100644 --- a/test/streams.test.ts +++ b/test/streams.test.ts @@ -1,16 +1,18 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { createReadStream } from 'fs' -import { resolve } from 'path' import t from 'tap' import { stream } from '../src' import { FastImageError, ImageInfo } from '../src/models' type Test = typeof t +const fileName = import.meta.url.replace('file://', '') +const imagePath = new URL('fixtures/image.png', import.meta.url).toString().replace('file://', '') + t.test('fastimage.stream', (t: Test) => { t.test('should emit info event when info are ready', (t: Test) => { - const input = createReadStream(resolve(__dirname, 'fixtures/image.png'), { highWaterMark: 200 }) + const input = createReadStream(imagePath, { highWaterMark: 200 }) const pipe = input.pipe(stream()) @@ -28,7 +30,7 @@ t.test('fastimage.stream', (t: Test) => { }) t.test('should emit error event in case of errors', (t: Test) => { - const input = createReadStream(__filename) + const input = createReadStream(fileName) const pipe = input.pipe(stream()) @@ -40,7 +42,7 @@ t.test('fastimage.stream', (t: Test) => { }) t.test('should accept the threshold option', (t: Test) => { - const input = createReadStream(resolve(__dirname, 'fixtures/image.png'), { highWaterMark: 1 }) + const input = createReadStream(imagePath, { highWaterMark: 1 }) const pipe = input.pipe(stream({ threshold: 10 })) diff --git a/test/urls.test.ts b/test/urls.test.ts index 04f120b..737f9eb 100644 --- a/test/urls.test.ts +++ b/test/urls.test.ts @@ -3,7 +3,6 @@ import { readFileSync } from 'fs' import { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http' import { AddressInfo, createServer, Socket } from 'net' -import { resolve } from 'path' import t from 'tap' import { info } from '../src' import { FastImageError, userAgentVersion } from '../src/models' @@ -22,10 +21,10 @@ t.test('fastimage.info', (t: Test) => { time: data.time, analyzed: data.analyzed, realUrl: 'https://fakeimg.pl/1000x1000/', - size: 17300 + size: 17308 }) - t.true(data.analyzed < data.size!) + t.ok(data.analyzed < data.size!) }) t.test('should return a error when the host cannot be found', async (t: Test) => { @@ -99,7 +98,7 @@ t.test('fastimage.info', (t: Test) => { const server = createHttpServer((r: IncomingMessage, s: ServerResponse) => { agents.push(r.headers['user-agent']!) - s.end(readFileSync(resolve(__dirname, 'fixtures/image.png'))) + s.end(readFileSync(new URL('fixtures/image.png', import.meta.url).toString().replace('file://', ''))) }) server.listen(0) diff --git a/tsconfig.json b/tsconfig.json index c081d98..064c767 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,11 @@ { "compilerOptions": { - "target": "es2019", - "module": "commonjs", + "target": "ES2020", + "module": "ESNext", "moduleResolution": "node", "jsx": "preserve", - "outDir": "dist/cjs", + "declaration": true, + "outDir": "dist", "declarationDir": "types", "allowJs": false, "allowSyntheticDefaultImports": true, @@ -13,8 +14,7 @@ "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, - "strictNullChecks": true, - "declaration": true + "strictNullChecks": true }, "include": ["src/*.ts"], "exclude": [] diff --git a/tsconfig.modules.json b/tsconfig.modules.json deleted file mode 100644 index ce9e69b..0000000 --- a/tsconfig.modules.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "ESNext", - "declaration": false, - "declarationDir": null, - "outDir": "dist/mjs" - } -} diff --git a/types/callback.d.ts b/types/callback.d.ts deleted file mode 100644 index 8c50477..0000000 --- a/types/callback.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ImageInfo } from './models'; -export declare type Callback = (error: Error | null, info?: ImageInfo) => void; -export declare function ensurePromiseCallback(callback?: Callback): [Callback, Promise?]; diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index df458c0..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -import { Stream, Writable, WritableOptions } from 'stream'; -import { Callback } from './callback'; -import { ImageInfo, Options } from './models'; -export declare function info(source: string | Stream | Buffer, options?: Partial | Callback, cb?: Callback): Promise; -export declare function stream(options?: Partial & Partial): Writable; diff --git a/types/internals.d.ts b/types/internals.d.ts deleted file mode 100644 index c74bf4b..0000000 --- a/types/internals.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -import { HTTPError, Response } from 'got'; -import { Stream } from 'stream'; -import { Callback } from './callback'; -import { FastImageError } from './models'; -export declare function toStream(source: string | Stream | Buffer, timeout: number, threshold: number, userAgent: string): [Stream, string | undefined]; -export declare function handleData(buffer: Buffer, response: Response | undefined, threshold: number, start: bigint, callback: Callback): boolean; -export declare function handleError(error: FastImageError | HTTPError, url: string): Error; diff --git a/types/models.d.ts b/types/models.d.ts deleted file mode 100644 index e6d386d..0000000 --- a/types/models.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface ImageInfo { - width: number; - height: number; - type: string; - time: number; - analyzed: number; - realUrl?: string; - size?: number; -} -export interface Options { - timeout: number; - threshold: number; - userAgent: string; -} -export declare class FastImageError extends Error { - code: string; - url?: string; - httpResponseCode?: number; - constructor(message: string, code: string, url?: string, httpResponseCode?: number); -} -export declare const userAgentVersion = "1.0.0"; -export declare const defaultOptions: Options; diff --git a/types/stream.d.ts b/types/stream.d.ts deleted file mode 100644 index 10d30fb..0000000 --- a/types/stream.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// -import { Writable, WritableOptions } from 'stream'; -import { Options } from './models'; -export declare class FastImageStream extends Writable { - buffer: Buffer; - threshold: number; - start: bigint; - finished: boolean; - constructor(options: Partial & WritableOptions); - analyze(chunk: Buffer): void; - _write(chunk: any, _e: BufferEncoding, cb: (error?: Error | null) => void): void; - _writev(chunks: Array<{ - chunk: any; - }>, cb: (error?: Error | null) => void): void; - _final(cb: (error?: Error | null) => void): void; -}