├── .github ├── dependabot.yaml └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE.md ├── README.md ├── examples └── example.js ├── factory.js ├── fetch.js ├── index.js ├── lib ├── get.js ├── put.js ├── resolveUrl.js └── response.js ├── package.json └── test ├── factory.test.js ├── fetch.test.js ├── get.test.js ├── put.test.js ├── resolveUrl.test.js ├── response.test.js ├── support ├── file.txt ├── isResponse.js └── urls.js └── test.js /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: npm 8 | directory: / 9 | schedule: 10 | interval: daily 11 | versioning-strategy: increase-if-necessary 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | test: 7 | runs-on: ubuntu-24.04 8 | strategy: 9 | matrix: 10 | node: 11 | - '18' 12 | - '20' 13 | - '22' 14 | - '23' 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - run: npm install 21 | - run: npm test 22 | - uses: coverallsapp/github-action@v2 23 | with: 24 | flag-name: run Node v${{ matrix.node }} 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | parallel: true 27 | finally: 28 | needs: test 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - uses: coverallsapp/github-action@v2 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | parallel-finished: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thomas Bergwinkl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # file-fetch 2 | 3 | [![build status](https://img.shields.io/github/actions/workflow/status/bergos/file-fetch/test.yaml?branch=master)](https://github.com/bergos/file-fetch/actions/workflows/test.yaml) 4 | [![npm version](https://img.shields.io/npm/v/file-fetch.svg)](https://www.npmjs.com/package/file-fetch) 5 | 6 | `file-fetch` is a [nodeify-fetch](https://www.npmjs.com/package/nodeify-fetch) compatible fetch for read and write access to the local file system using `file:` URLs and URIs (including 7 | implicit ones using relative paths). 8 | 9 | ## Usage 10 | 11 | ### Read 12 | 13 | Reading a file from the file system is as easy as fetching it on the Web. 14 | Call `fetch` with the URL, and the content is provided as `Readable` stream in `res.body`. 15 | The example below uses an absolute URL, but relative paths are also supported. 16 | See the [Supported URLs and URIs](#supported-urls-and-uris) section for more details. 17 | 18 | ```js 19 | import fetch from 'file-fetch' 20 | 21 | const res = await fetch(new URL('example.js', import.meta.url)) 22 | 23 | res.body.pipe(process.stdout) 24 | ``` 25 | 26 | It's also possible to handle the content without streams. 27 | The async `res.text()` method returns the whole content as a string. 28 | 29 | ```js 30 | import fetch from 'file-fetch' 31 | 32 | const res = await fetch(new URL('example.js', import.meta.url)) 33 | 34 | console.log(await res.text()) 35 | ``` 36 | 37 | A similar method `res.json()` is available to parse JSON content and return the parsed result. 38 | 39 | ```js 40 | import fetch from 'file-fetch' 41 | 42 | const res = await fetch(new URL('example.js', import.meta.url)) 43 | 44 | console.log(await res.json()) 45 | ``` 46 | 47 | ### Write 48 | 49 | Writing content to a file is done with the same function but with the `PUT` method. 50 | The content must be provided as a `string` or a `Readable` stream object. 51 | 52 | ```js 53 | import fetch from 'file-fetch' 54 | 55 | await fetch('file:///tmp/example.log', { 56 | method: 'PUT', 57 | body: 'test' 58 | }) 59 | ``` 60 | 61 | ```js 62 | import fetch from 'file-fetch' 63 | import { Readable } from 'readable-stream' 64 | 65 | await fetch('file:///tmp/example.log', { 66 | method: 'PUT', 67 | body: Readable.from(['test']) 68 | }) 69 | ``` 70 | 71 | ## Options 72 | 73 | `file-fetch` supports the following non-standard options: 74 | 75 | - `baseURL`: A `string` or `URL` used to resolve relative paths and URIs. 76 | - `contentType`: A `string` or `function` to determine the media type based on the file extension or a fixed value. 77 | It can be useful if file extensions or media types not covered by [mime-db](https://www.npmjs.com/package/mime-db) are required. 78 | 79 | ## Custom fetch with fixed baseURL or contentType lookup 80 | 81 | Custom fetch instances can be useful if requests should be processed with relative paths to a directory that is not the current working directory. 82 | The `contentType` argument can also be predefined for the instance. 83 | The example below shows how to set the `baseURL` to a relative path of the current script and how to use a custom `contentType` function: 84 | 85 | ```js 86 | import { factory as fetchFactory } from 'file-fetch' 87 | 88 | const baseURL = new URL('examples', import.meta.url) 89 | const contentType = ext => ext === 'json' ? 'application/ld+json' : 'application/octet-stream' 90 | 91 | const fetch = fetchFactory({ baseURL, contentType }) 92 | 93 | const res = await fetch('example.js') 94 | const text = await res.text() 95 | ``` 96 | 97 | ## Supported URLs and URIs 98 | 99 | Different styles of URLs and URIs are supported. 100 | 101 | ### Absolute URLs 102 | 103 | An absolute URL for a `file` schema must start with `file:///`. 104 | No further resolve logic is used. 105 | 106 | Example: 107 | 108 | ``` 109 | file:///home/user/tmp/content.txt 110 | ``` 111 | 112 | ### URIs 113 | 114 | URIs are supported for use cases where a `file` scheme is required to distinguish identifiers by scheme and if relative paths are required. 115 | The [relative paths](#relative-paths) logic is used to resolve the full URL. 116 | 117 | Example: 118 | 119 | ``` 120 | file:tmp/content.txt 121 | ``` 122 | 123 | ### Relative paths 124 | 125 | Relative paths are resolved with the given `baseURL` or, if not given, with the working directory. 126 | 127 | Example: 128 | 129 | ``` 130 | tmp/content.txt 131 | ``` 132 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | import fetch, { factory as fetchFactory } from '../index.js' 2 | 3 | async function readFileAbsoluteUrl () { 4 | const res = await fetch(new URL('example.js', import.meta.url)) 5 | const text = await res.text() 6 | 7 | console.log(`read ${text.length} chars from file with absolute url`) 8 | } 9 | 10 | async function readFileBaseUrl () { 11 | const fetch = fetchFactory({ baseURL: import.meta.url }) 12 | const res = await fetch('example.js') 13 | const text = await res.text() 14 | 15 | console.log(`read ${text.length} chars from file with baseURL`) 16 | } 17 | 18 | async function readFilePipe () { 19 | const res = await fetch(new URL('example.js', import.meta.url)) 20 | 21 | console.log('read pipe content to stdout') 22 | 23 | res.body.pipe(process.stdout) 24 | } 25 | 26 | await readFileAbsoluteUrl() 27 | await readFileBaseUrl() 28 | await readFilePipe() 29 | -------------------------------------------------------------------------------- /factory.js: -------------------------------------------------------------------------------- 1 | import fetch from './fetch.js' 2 | 3 | function factory ({ baseURL, contentType } = {}) { 4 | return (uri, options) => { 5 | return fetch(uri, { baseURL, contentType, ...options }) 6 | } 7 | } 8 | 9 | export default factory 10 | -------------------------------------------------------------------------------- /fetch.js: -------------------------------------------------------------------------------- 1 | import get from './lib/get.js' 2 | import put from './lib/put.js' 3 | import resolveUrl from './lib/resolveUrl.js' 4 | import response from './lib/response.js' 5 | 6 | async function fetch (uri, { baseURL, body, contentType, method = 'GET' } = {}) { 7 | method = method.toUpperCase() 8 | 9 | const url = resolveUrl(uri.toString(), baseURL) 10 | 11 | if (method === 'GET') { 12 | return get(url, { contentType }) 13 | } 14 | 15 | if (method === 'HEAD') { 16 | return get(url, { contentType, method: 'HEAD' }) 17 | } 18 | 19 | if (method === 'PUT') { 20 | return put(url, { body }) 21 | } 22 | 23 | return response(405) 24 | } 25 | 26 | export default fetch 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import factory from './factory.js' 2 | import fetch from './fetch.js' 3 | 4 | const Headers = globalThis.Headers 5 | 6 | export { 7 | fetch as default, 8 | factory, 9 | Headers 10 | } 11 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | import { createReadStream } from 'node:fs' 2 | import { stat } from 'node:fs/promises' 3 | import { extname } from 'node:path' 4 | import { contentType as defaultContentType } from 'mime-types' 5 | import response from './response.js' 6 | 7 | async function fileSize (url) { 8 | try { 9 | return (await stat(url)).size 10 | } catch (error) { 11 | return null 12 | } 13 | } 14 | 15 | async function get (url, { contentType = defaultContentType, method = 'GET' } = {}) { 16 | const size = await fileSize(url) 17 | 18 | if (size === null) { 19 | return response(404, {}, new Error('Not Found')) 20 | } 21 | 22 | const extension = extname(url.pathname) 23 | 24 | if (typeof contentType === 'function') { 25 | contentType = contentType(extension) 26 | } 27 | 28 | if (!contentType) { 29 | contentType = 'application/octet-stream' 30 | } 31 | 32 | const ok = stream => { 33 | return response(200, { 34 | 'content-length': size.toString(), 35 | 'content-type': contentType 36 | }, stream) 37 | } 38 | 39 | if (method === 'GET') { 40 | return new Promise(resolve => { 41 | const stream = createReadStream(url) 42 | 43 | stream 44 | .on('open', () => resolve(ok(stream))) 45 | .on('error', err => resolve(response(500, {}, err))) 46 | }) 47 | } 48 | 49 | if (method === 'HEAD') { 50 | return ok() 51 | } 52 | } 53 | 54 | export default get 55 | -------------------------------------------------------------------------------- /lib/put.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'node:fs' 2 | import { Readable } from 'readable-stream' 3 | import response from './response.js' 4 | 5 | async function put (url, { body }) { 6 | if (body === undefined || body === null) { 7 | return response(400) 8 | } 9 | 10 | if (typeof body === 'string') { 11 | body = Readable.from([body]) 12 | } 13 | 14 | return new Promise(resolve => { 15 | body.pipe(createWriteStream(url)) 16 | .on('finish', () => resolve(response(201))) 17 | .on('error', err => resolve(response(500, {}, err))) 18 | }) 19 | } 20 | 21 | export default put 22 | -------------------------------------------------------------------------------- /lib/resolveUrl.js: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url' 2 | 3 | function resolveUrl (url, baseURL = pathToFileURL(`${process.cwd()}/`)) { 4 | const schemaMatch = url.match(/^([a-z]+):/) 5 | 6 | // non-file URIs 7 | if (schemaMatch && schemaMatch[1] !== 'file') { 8 | throw new Error(`unknown schema: ${url}`) 9 | } 10 | 11 | // path 12 | if (!schemaMatch) { 13 | return new URL(url, baseURL) 14 | } 15 | 16 | // URLs 17 | if (url.startsWith('file:///')) { 18 | return new URL(url) 19 | } 20 | 21 | // URIs 22 | return new URL(url.slice(5), baseURL) 23 | } 24 | 25 | export default resolveUrl 26 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | import { Readable } from 'readable-stream' 2 | import decode from 'stream-chunks/decode.js' 3 | 4 | const statusTexts = new Map([ 5 | [200, 'OK'], 6 | [201, 'Created'], 7 | [400, 'Bad Request'], 8 | [404, 'Not Found'], 9 | [405, 'Method Not Allowed'], 10 | [500, 'Internal Server Error'] 11 | ]) 12 | 13 | function response (status, headers, body) { 14 | headers = new globalThis.Headers(headers || {}) 15 | 16 | if (body instanceof Error) { 17 | const err = body 18 | 19 | headers.set('content-type', 'application/json') 20 | 21 | body = Readable.from([JSON.stringify({ 22 | title: err.message 23 | })]) 24 | } 25 | 26 | const statusText = statusTexts.get(status) 27 | const ok = status >= 200 && status <= 299 28 | const json = body && (async () => JSON.parse(await decode(body))) 29 | const text = body && (async () => decode(body)) 30 | 31 | return { 32 | status, 33 | statusText, 34 | ok, 35 | headers, 36 | body, 37 | json, 38 | text 39 | } 40 | } 41 | 42 | export default response 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-fetch", 3 | "version": "2.0.1", 4 | "description": "fetch for read and write access to the local file system", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "stricter-standard && c8 --reporter=lcov --reporter=text-summary mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/bergos/file-fetch.git" 13 | }, 14 | "keywords": [ 15 | "fetch", 16 | "readable", 17 | "stream", 18 | "file", 19 | "fs" 20 | ], 21 | "author": "Thomas Bergwinkl (https://www.bergnet.org/people/bergi/card#me)", 22 | "contributors": [ 23 | "Brett Zamir" 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/bergos/file-fetch/issues" 28 | }, 29 | "homepage": "https://github.com/bergos/file-fetch", 30 | "dependencies": { 31 | "mime-types": "^3.0.1", 32 | "readable-stream": "^4.4.2", 33 | "stream-chunks": "^1.0.0" 34 | }, 35 | "devDependencies": { 36 | "c8": "^10.1.2", 37 | "is-stream": "^4.0.1", 38 | "mocha": "^11.0.1", 39 | "stricter-standard": "^0.3.0", 40 | "temp": "^0.9.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/factory.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import factory from '../factory.js' 4 | import urls from './support/urls.js' 5 | 6 | describe('factory', () => { 7 | it('should be a function', () => { 8 | strictEqual(typeof factory, 'function') 9 | }) 10 | 11 | it('should forward the baseURL argument', async () => { 12 | const fetch = factory({ baseURL: urls.supportDir }) 13 | const res = await fetch(urls.fileTxtRelative) 14 | const result = await res.text() 15 | 16 | strictEqual(result, 'test') 17 | }) 18 | 19 | it('should forward the contentType argument', async () => { 20 | const contentType = () => 'application/json' 21 | const fetch = factory({ contentType }) 22 | const res = await fetch(urls.fileTxt) 23 | 24 | strictEqual(res.headers.get('content-type'), 'application/json') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/fetch.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { readFile } from 'node:fs/promises' 3 | import { describe, it } from 'mocha' 4 | import temp from 'temp' 5 | import fetch from '../fetch.js' 6 | import urls from './support/urls.js' 7 | 8 | temp.track() 9 | 10 | describe('fetch', () => { 11 | it('should be a function', () => { 12 | strictEqual(typeof fetch, 'function') 13 | }) 14 | 15 | it('should use GET method if no method is given', async () => { 16 | const res = await fetch(urls.fileTxt) 17 | const result = await res.text() 18 | 19 | strictEqual(result, 'test') 20 | }) 21 | 22 | it('should support GET method calls', async () => { 23 | const res = await fetch(urls.fileTxt, { method: 'GET' }) 24 | const result = await res.text() 25 | 26 | strictEqual(result, 'test') 27 | }) 28 | 29 | it('should support HEAD method calls', async () => { 30 | const res = await fetch(urls.fileTxt, { method: 'HEAD' }) 31 | 32 | strictEqual(typeof res.body, 'undefined') 33 | strictEqual(res.headers.get('content-length'), '4') 34 | }) 35 | 36 | it('should support PUT method calls', async () => { 37 | const path = temp.path() 38 | 39 | await fetch(path, { body: 'test', method: 'PUT' }) 40 | const result = (await readFile(path)).toString() 41 | 42 | strictEqual(result, 'test') 43 | }) 44 | 45 | it('should translate method values to upper case', async () => { 46 | const res = await fetch(urls.fileTxt, { method: 'hEAd' }) 47 | 48 | strictEqual(typeof res.body, 'undefined') 49 | strictEqual(res.headers.get('content-length'), '4') 50 | }) 51 | 52 | it('should use the baseURL to resolve URLs', async () => { 53 | const res = await fetch(urls.fileTxtRelative, { baseURL: urls.supportDir }) 54 | const result = await res.text() 55 | 56 | strictEqual(result, 'test') 57 | }) 58 | 59 | it('should return a response with a 405 status code if an unknown method is given', async () => { 60 | const res = await fetch(urls.fileTxt, { method: 'OPTION' }) 61 | 62 | strictEqual(res.status, 405) 63 | }) 64 | 65 | it('should forward the contentType argument', async () => { 66 | const contentType = () => 'application/json' 67 | const res = await fetch(urls.fileTxt, { contentType }) 68 | 69 | strictEqual(res.headers.get('content-type'), 'application/json') 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/get.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import { decode } from 'stream-chunks' 4 | import get from '../lib/get.js' 5 | import { isResponseWithBody } from './support/isResponse.js' 6 | import urls from './support/urls.js' 7 | 8 | describe('get', () => { 9 | it('should be a function', () => { 10 | strictEqual(typeof get, 'function') 11 | }) 12 | 13 | it('should return a response object', async () => { 14 | const res = await get(urls.fileTxt) 15 | 16 | isResponseWithBody(res) 17 | }) 18 | 19 | it('should set the content-type header based on the file extension', async () => { 20 | const res = await get(urls.fileTxt) 21 | const result = res.headers.get('content-type').split(';')[0] 22 | 23 | strictEqual(result, 'text/plain') 24 | }) 25 | 26 | it('should set the content-type header based on the given contentType string value', async () => { 27 | const res = await get(urls.fileTxt, { contentType: 'application/json' }) 28 | const result = res.headers.get('content-type').split(';')[0] 29 | 30 | strictEqual(result, 'application/json') 31 | }) 32 | 33 | it('should set the content-type header based on the result value of the contentType function', async () => { 34 | const contentType = () => 'application/ld+json' 35 | 36 | const res = await get(urls.fileTxt, { contentType }) 37 | const result = res.headers.get('content-type').split(';')[0] 38 | 39 | strictEqual(result, 'application/ld+json') 40 | }) 41 | 42 | it('should set the content-type header to the default value of no media type was found', async () => { 43 | const contentType = () => undefined 44 | 45 | const res = await get(urls.fileTxt, { contentType }) 46 | const result = res.headers.get('content-type').split(';')[0] 47 | 48 | strictEqual(result, 'application/octet-stream') 49 | }) 50 | 51 | it('should set the content-length header to the number of bytes in the file', async () => { 52 | const res = await get(urls.fileTxt) 53 | 54 | const result = res.headers.get('content-length') 55 | 56 | strictEqual(result, '4') 57 | }) 58 | 59 | it('should have a readable stream as body that emits the file content if no method is given', async () => { 60 | const res = await get(urls.fileTxt) 61 | const result = await decode(res.body) 62 | 63 | strictEqual(result, 'test') 64 | }) 65 | 66 | it('should not have a body if the method is HEAD', async () => { 67 | const res = await get(urls.fileTxt, { method: 'HEAD' }) 68 | 69 | strictEqual(typeof res.body, 'undefined') 70 | }) 71 | 72 | it('should set the status to 404 if the url points to a non-existent file', async () => { 73 | const res = await get(urls.missingTxt) 74 | 75 | strictEqual(res.status, 404) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/put.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { readFile, writeFile } from 'node:fs/promises' 3 | import { describe, it } from 'mocha' 4 | import { Readable } from 'readable-stream' 5 | import temp from 'temp' 6 | import put from '../lib/put.js' 7 | import { isResponse } from './support/isResponse.js' 8 | 9 | temp.track() 10 | 11 | describe('put', () => { 12 | it('should be a function', () => { 13 | strictEqual(typeof put, 'function') 14 | }) 15 | 16 | it('should return a response object', async () => { 17 | const res = await put(temp.path(), { body: '' }) 18 | 19 | isResponse(res) 20 | }) 21 | 22 | it('should write the content of the text body to the file', async () => { 23 | const path = temp.path() 24 | 25 | await put(path, { body: 'test' }) 26 | const result = (await readFile(path)).toString() 27 | 28 | strictEqual(result, 'test') 29 | }) 30 | 31 | it('should write the content of the stream body to the file', async () => { 32 | const path = temp.path() 33 | 34 | await put(path, { body: Readable.from(['test']) }) 35 | const result = (await readFile(path)).toString() 36 | 37 | strictEqual(result, 'test') 38 | }) 39 | 40 | it('should overwrite existing content', async () => { 41 | const path = temp.path() 42 | await writeFile(path, 'test') 43 | 44 | await put(path, { body: 'text' }) 45 | const result = (await readFile(path)).toString() 46 | 47 | strictEqual(result, 'text') 48 | }) 49 | 50 | it('should set the status to 400 no body was given', async () => { 51 | const res = await put(temp.path(), {}) 52 | 53 | strictEqual(res.status, 400) 54 | }) 55 | 56 | it('should set the status to 500 if the url points to a directory', async () => { 57 | const res = await put(await temp.mkdir(), { body: '' }) 58 | 59 | strictEqual(res.status, 500) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/resolveUrl.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual, throws } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import resolveUrl from '../lib/resolveUrl.js' 4 | 5 | describe('resolveUrl', () => { 6 | it('should be a function', () => { 7 | strictEqual(typeof resolveUrl, 'function') 8 | }) 9 | 10 | it('should throw an error if a non-file URI is given', () => { 11 | throws(() => { 12 | resolveUrl('mailto:example@example.org') 13 | }) 14 | }) 15 | 16 | it('should throw an error if a non-file URL is given', () => { 17 | throws(() => { 18 | resolveUrl('http://example.org/') 19 | }) 20 | }) 21 | 22 | it('should extend a pathname with the default baseURL', () => { 23 | const expected = `file://${process.cwd()}/path/file.txt` 24 | const result = resolveUrl('path/file.txt') 25 | 26 | strictEqual(result.toString(), expected) 27 | }) 28 | 29 | it('should extend a pathname with the given baseURL', () => { 30 | const expected = 'file:///root/path/file.txt' 31 | const result = resolveUrl('path/file.txt', 'file:///root/') 32 | 33 | strictEqual(result.toString(), expected) 34 | }) 35 | 36 | it('should forward URLs without changes', () => { 37 | const expected = 'file:///root/file.txt' 38 | const result = resolveUrl(expected) 39 | 40 | strictEqual(result.toString(), expected) 41 | }) 42 | 43 | it('should extend a URI with the default baseURL', () => { 44 | const expected = `file://${process.cwd()}/path/file.txt` 45 | const result = resolveUrl('file:path/file.txt') 46 | 47 | strictEqual(result.toString(), expected) 48 | }) 49 | 50 | it('should extend a URI with the given baseURL', () => { 51 | const expected = 'file:///root/path/file.txt' 52 | const result = resolveUrl('file:path/file.txt', 'file:///root/') 53 | 54 | strictEqual(result.toString(), expected) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/response.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, strictEqual } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import { Readable } from 'readable-stream' 4 | import { decode } from 'stream-chunks' 5 | import response from '../lib/response.js' 6 | import { isResponse } from './support/isResponse.js' 7 | 8 | describe('response', () => { 9 | it('should be a function', () => { 10 | strictEqual(typeof response, 'function') 11 | }) 12 | 13 | it('should return a response object', () => { 14 | isResponse(response(200)) 15 | }) 16 | 17 | it('should assign the given status', () => { 18 | const res = response(200) 19 | 20 | strictEqual(res.status, 200) 21 | }) 22 | 23 | it('should set the statusText matching the 200 status', () => { 24 | const res = response(200) 25 | 26 | strictEqual(res.statusText, 'OK') 27 | }) 28 | 29 | it('should set the statusText matching the 201 status', () => { 30 | const res = response(201) 31 | 32 | strictEqual(res.statusText, 'Created') 33 | }) 34 | 35 | it('should set the statusText matching the 400 status', () => { 36 | const res = response(400) 37 | 38 | strictEqual(res.statusText, 'Bad Request') 39 | }) 40 | 41 | it('should set the statusText matching the 404 status', () => { 42 | const res = response(405) 43 | 44 | strictEqual(res.statusText, 'Method Not Allowed') 45 | }) 46 | 47 | it('should set the statusText matching the 404 status', () => { 48 | const res = response(404) 49 | 50 | strictEqual(res.statusText, 'Not Found') 51 | }) 52 | 53 | it('should set the statusText matching the 500 status', () => { 54 | const res = response(500) 55 | 56 | strictEqual(res.statusText, 'Internal Server Error') 57 | }) 58 | 59 | it('should set ok to true for a 200 status', () => { 60 | const res = response(200) 61 | 62 | strictEqual(res.ok, true) 63 | }) 64 | 65 | it('should set ok to false for a 400 status', () => { 66 | const res = response(400) 67 | 68 | strictEqual(res.ok, false) 69 | }) 70 | 71 | it('should assign the given headers', () => { 72 | const res = response(200, { a: 'b', c: 'd' }) 73 | 74 | strictEqual(res.headers.get('a'), 'b') 75 | strictEqual(res.headers.get('c'), 'd') 76 | }) 77 | 78 | it('should assign the given body', async () => { 79 | const res = response(200, {}, Readable.from(['test'])) 80 | 81 | const result = await decode(res.body) 82 | 83 | strictEqual(result, 'test') 84 | }) 85 | 86 | it('should convert and assign an error object', async () => { 87 | const res = response(500, {}, new Error('test')) 88 | 89 | const contentType = res.headers.get('content-type') 90 | const body = await decode(res.body) 91 | 92 | strictEqual(contentType, 'application/json') 93 | strictEqual(body, JSON.stringify({ title: 'test' })) 94 | }) 95 | 96 | it('should assign the json method if a body is given', async () => { 97 | const res = response(200, {}, Readable.from(['{}'])) 98 | 99 | strictEqual(typeof res.json, 'function') 100 | }) 101 | 102 | it('should parse and return a given json body', async () => { 103 | const res = response(200, {}, Readable.from(['{}'])) 104 | 105 | const result = await res.json() 106 | 107 | deepStrictEqual(result, {}) 108 | }) 109 | 110 | it('should assign the text method if a body is given', async () => { 111 | const res = response(200, {}, Readable.from(['test'])) 112 | 113 | strictEqual(typeof res.text, 'function') 114 | }) 115 | 116 | it('should return a given body as string', async () => { 117 | const res = response(200, {}, Readable.from(['test'])) 118 | 119 | const result = await res.text() 120 | 121 | deepStrictEqual(result, 'test') 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/support/file.txt: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/support/isResponse.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { isReadableStream } from 'is-stream' 3 | 4 | function isResponse (res) { 5 | strictEqual(typeof res, 'object') 6 | strictEqual(typeof res.status, 'number') 7 | strictEqual(typeof res.ok, 'boolean') 8 | strictEqual(typeof res.headers, 'object') 9 | } 10 | 11 | function isResponseWithBody (res) { 12 | isResponse(res) 13 | 14 | strictEqual(isReadableStream(res.body), true) 15 | strictEqual(typeof res.text, 'function') 16 | strictEqual(typeof res.json, 'function') 17 | } 18 | 19 | export { 20 | isResponse, 21 | isResponseWithBody 22 | } 23 | -------------------------------------------------------------------------------- /test/support/urls.js: -------------------------------------------------------------------------------- 1 | const urls = { 2 | fileTxtRelative: 'file.txt', 3 | fileTxt: new URL('file.txt', import.meta.url), 4 | supportDir: new URL('.', import.meta.url) 5 | } 6 | 7 | export default urls 8 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import fetch, { factory, Headers } from '../index.js' 4 | import urls from './support/urls.js' 5 | 6 | describe('fileFetch', () => { 7 | it('should export fetch as default', async () => { 8 | const res = await fetch(urls.fileTxt) 9 | const result = await res.text() 10 | 11 | strictEqual(result, 'test') 12 | }) 13 | 14 | it('should export factory', async () => { 15 | const fetch = factory({ baseURL: urls.supportDir }) 16 | const res = await fetch(urls.fileTxtRelative) 17 | const result = await res.text() 18 | 19 | strictEqual(result, 'test') 20 | }) 21 | 22 | it('should export Headers', () => { 23 | const headers = new Headers({ 24 | a: 'b', 25 | c: 'd' 26 | }) 27 | 28 | headers.set('e', 'f') 29 | 30 | strictEqual(headers.get('a'), 'b') 31 | strictEqual(headers.get('c'), 'd') 32 | strictEqual(headers.get('e'), 'f') 33 | }) 34 | }) 35 | --------------------------------------------------------------------------------