diff --git a/readme.md b/readme.md index 9826868a..dd4257fe 100644 --- a/readme.md +++ b/readme.md @@ -149,7 +149,7 @@ Returns a [`Response` object](https://developer.mozilla.org/en-US/docs/Web/API/R Sets `options.method` to the method name and makes a request. -When using a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance as `input`, any URL altering options (such as `prefixUrl`) will be ignored. +When using a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance as `input`, any URL altering options (such as `startPath`) will be ignored. #### options @@ -181,7 +181,7 @@ Search parameters to include in the request URL. Setting this will override all Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams). -##### prefixUrl +##### startPath Type: `string | URL` @@ -194,16 +194,16 @@ import ky from 'ky'; // On https://example.com -const response = await ky('unicorn', {prefixUrl: '/api'}); +const response = await ky('unicorn', {startPath: '/api'}); //=> 'https://example.com/api/unicorn' -const response2 = await ky('unicorn', {prefixUrl: 'https://cats.com'}); +const response2 = await ky('unicorn', {startPath: 'https://cats.com'}); //=> 'https://cats.com/unicorn' ``` Notes: - - After `prefixUrl` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any). - - Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `prefixUrl` is being used, which changes the meaning of a leading slash. + - After `startPath` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any). + - Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `startPath` is being used, which changes the meaning of a leading slash. ##### retry @@ -519,12 +519,12 @@ import ky from 'ky'; // On https://my-site.com -const api = ky.create({prefixUrl: 'https://example.com/api'}); +const api = ky.create({startPath: 'https://example.com/api'}); const response = await api.get('users/123'); //=> 'https://example.com/api/users/123' -const response = await api.get('/status', {prefixUrl: ''}); +const response = await api.get('/status', {startPath: ''}); //=> 'https://my-site.com/status' ``` diff --git a/source/core/Ky.ts b/source/core/Ky.ts index 5d64cb80..a1a01151 100644 --- a/source/core/Ky.ts +++ b/source/core/Ky.ts @@ -1,7 +1,9 @@ import {HTTPError} from '../errors/HTTPError.js'; import {TimeoutError} from '../errors/TimeoutError.js'; import type {Hooks} from '../types/hooks.js'; -import type {Input, InternalOptions, NormalizedOptions, Options, SearchParamsInit} from '../types/options.js'; +import type { + Input, InternalOptions, NormalizedOptions, Options, SearchParamsInit, +} from '../types/options.js'; import {type ResponsePromise} from '../types/ResponsePromise.js'; import {deepMerge, mergeHeaders} from '../utils/merge.js'; import {normalizeRequestMethod, normalizeRetryOptions} from '../utils/normalize.js'; @@ -137,9 +139,9 @@ export class Ky { options.hooks, ), method: normalizeRequestMethod(options.method ?? (this._input as Request).method), - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - prefixUrl: String(options.prefixUrl || ''), retry: normalizeRetryOptions(options.retry), + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + startPath: String(options.startPath || ''), throwHttpErrors: options.throwHttpErrors !== false, timeout: options.timeout ?? 10_000, fetch: options.fetch ?? globalThis.fetch.bind(globalThis), @@ -149,16 +151,22 @@ export class Ky { throw new TypeError('`input` must be a string, URL, or Request'); } - if (this._options.prefixUrl && typeof this._input === 'string') { - if (this._input.startsWith('/')) { - throw new Error('`input` must not begin with a slash when using `prefixUrl`'); - } + if (typeof this._input === 'string') { + if (this._options.startPath) { + if (!this._options.startPath.endsWith('/')) { + this._options.startPath += '/'; + } - if (!this._options.prefixUrl.endsWith('/')) { - this._options.prefixUrl += '/'; + if (this._input.startsWith('/')) { + this._input = this._input.slice(1); + } + + this._input = this._options.startPath + this._input; } - this._input = this._options.prefixUrl + this._input; + if (this._options.baseUrl) { + this._input = new URL(this._input, (new Request(options.baseUrl)).url); + } } if (supportsAbortController) { diff --git a/source/core/constants.ts b/source/core/constants.ts index 86055489..c4f559fb 100644 --- a/source/core/constants.ts +++ b/source/core/constants.ts @@ -61,7 +61,7 @@ export const kyOptionKeys: KyOptionsRegistry = { parseJson: true, stringifyJson: true, searchParams: true, - prefixUrl: true, + startPath: true, retry: true, timeout: true, hooks: true, diff --git a/source/types/options.ts b/source/types/options.ts index 22fd0651..a7a4e54d 100644 --- a/source/types/options.ts +++ b/source/types/options.ts @@ -97,8 +97,8 @@ export type KyOptions = { Useful when used with [`ky.extend()`](#kyextenddefaultoptions) to create niche-specific Ky-instances. Notes: - - After `prefixUrl` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any). - - Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `prefixUrl` is being used, which changes the meaning of a leading slash. + - After `startPath` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any). + - Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `startPath` is being used, which changes the meaning of a leading slash. @example ``` @@ -106,14 +106,14 @@ export type KyOptions = { // On https://example.com - const response = await ky('unicorn', {prefixUrl: '/api'}); + const response = await ky('unicorn', {startPath: '/api'}); //=> 'https://example.com/api/unicorn' - const response = await ky('unicorn', {prefixUrl: 'https://cats.com'}); + const response = await ky('unicorn', {startPath: 'https://cats.com'}); //=> 'https://cats.com/unicorn' ``` */ - prefixUrl?: URL | string; + startPath?: URL | string; /** An object representing `limit`, `methods`, `statusCodes` and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time. @@ -265,12 +265,12 @@ export interface Options extends KyOptions, Omit { // es export type InternalOptions = Required< Omit, -'fetch' | 'prefixUrl' | 'timeout' +'fetch' | 'startPath' | 'timeout' > & { headers: Required; hooks: Required; retry: Required; - prefixUrl: string; + startPath: string; }; /** @@ -283,7 +283,7 @@ export interface NormalizedOptions extends RequestInit { // eslint-disable-line // Extended from custom `KyOptions`, but ensured to be set (not optional). retry: RetryOptions; - prefixUrl: string; + startPath: string; onDownloadProgress: Options['onDownloadProgress']; } diff --git a/test/base-url.ts b/test/base-url.ts new file mode 100644 index 00000000..4a614d22 --- /dev/null +++ b/test/base-url.ts @@ -0,0 +1,39 @@ +import test from 'ava'; +import ky from '../source/index.js'; +import {createHttpTestServer} from './helpers/create-http-test-server.js'; + +test('baseUrl option', async t => { + const server = await createHttpTestServer(); + server.get('/', (_request, response) => { + response.end('/'); + }); + server.get('/foo', (_request, response) => { + response.end('/foo'); + }); + server.get('/bar', (_request, response) => { + response.end('/bar'); + }); + server.get('/foo/bar', (_request, response) => { + response.end('/foo/bar'); + }); + + t.is( + // @ts-expect-error {baseUrl: boolean} isn't officially supported + await ky(`${server.url}/foo/bar`, {baseUrl: false}).text(), + '/foo/bar', + ); + t.is(await ky(`${server.url}/foo/bar`, {baseUrl: ''}).text(), '/foo/bar'); + t.is(await ky(new URL(`${server.url}/foo/bar`), {baseUrl: ''}).text(), '/foo/bar'); + t.is(await ky('foo/bar', {baseUrl: server.url}).text(), '/foo/bar'); + t.is(await ky('foo/bar', {baseUrl: new URL(server.url)}).text(), '/foo/bar'); + t.is(await ky('/bar', {baseUrl: `${server.url}/foo/`}).text(), '/bar'); + t.is(await ky('/bar', {baseUrl: `${server.url}/foo`}).text(), '/bar'); + t.is(await ky('bar', {baseUrl: `${server.url}/foo/`}).text(), '/foo/bar'); + t.is(await ky('bar', {baseUrl: `${server.url}/foo`}).text(), '/bar'); + t.is(await ky('bar', {baseUrl: new URL(`${server.url}/foo`)}).text(), '/bar'); + t.is(await ky('', {baseUrl: server.url}).text(), '/'); + t.is(await ky('', {baseUrl: `${server.url}/`}).text(), '/'); + t.is(await ky('', {baseUrl: new URL(server.url)}).text(), '/'); + + await server.close(); +}); diff --git a/test/browser.ts b/test/browser.ts index 088a0b56..e7da2a75 100644 --- a/test/browser.ts +++ b/test/browser.ts @@ -47,7 +47,7 @@ test.afterEach(async () => { await server.close(); }); -defaultBrowsersTest('prefixUrl option', async (t: ExecutionContext, page: Page) => { +defaultBrowsersTest('startPath option', async (t: ExecutionContext, page: Page) => { server.get('/', (_request, response) => { response.end('zebra'); }); @@ -60,16 +60,16 @@ defaultBrowsersTest('prefixUrl option', async (t: ExecutionContext, page: Page) await addKyScriptToPage(page); await t.throwsAsync( - page.evaluate(async () => window.ky('/foo', {prefixUrl: '/'})), - {message: /`input` must not begin with a slash when using `prefixUrl`/}, + page.evaluate(async () => window.ky('/foo', {startPath: '/'})), + {message: /`input` must not begin with a slash when using `startPath`/}, ); const results = await page.evaluate(async (url: string) => Promise.all([ window.ky(`${url}/api/unicorn`).text(), - // @ts-expect-error unsupported {prefixUrl: null} type - window.ky(`${url}/api/unicorn`, {prefixUrl: null}).text(), - window.ky('api/unicorn', {prefixUrl: url}).text(), - window.ky('api/unicorn', {prefixUrl: `${url}/`}).text(), + // @ts-expect-error unsupported {startPath: null} type + window.ky(`${url}/api/unicorn`, {startPath: null}).text(), + window.ky('api/unicorn', {startPath: url}).text(), + window.ky('api/unicorn', {startPath: `${url}/`}).text(), ]), server.url); t.deepEqual(results, ['rainbow', 'rainbow', 'rainbow', 'rainbow']); diff --git a/test/fetch.ts b/test/fetch.ts index 05712087..5f89de69 100644 --- a/test/fetch.ts +++ b/test/fetch.ts @@ -43,7 +43,7 @@ test('fetch option takes a custom fetch function', async t => { }).text(), `${fixture}?new#hash`, ); - t.is(await ky('unicorn', {fetch: customFetch, prefixUrl: `${fixture}/api/`}).text(), `${fixture}/api/unicorn`); + t.is(await ky('unicorn', {fetch: customFetch, startPath: `${fixture}/api/`}).text(), `${fixture}/api/unicorn`); }); test('options are correctly passed to Fetch #1', async t => { diff --git a/test/prefix-url.ts b/test/prefix-url.ts deleted file mode 100644 index e09650b4..00000000 --- a/test/prefix-url.ts +++ /dev/null @@ -1,40 +0,0 @@ -import test from 'ava'; -import ky from '../source/index.js'; -import {createHttpTestServer} from './helpers/create-http-test-server.js'; - -test('prefixUrl option', async t => { - const server = await createHttpTestServer(); - server.get('/', (_request, response) => { - response.end('zebra'); - }); - server.get('/api/unicorn', (_request, response) => { - response.end('rainbow'); - }); - - t.is( - // @ts-expect-error {prefixUrl: boolean} isn't officially supported - await ky(`${server.url}/api/unicorn`, {prefixUrl: false}).text(), - 'rainbow', - ); - t.is(await ky(`${server.url}/api/unicorn`, {prefixUrl: ''}).text(), 'rainbow'); - t.is(await ky(new URL(`${server.url}/api/unicorn`), {prefixUrl: ''}).text(), 'rainbow'); - t.is(await ky('api/unicorn', {prefixUrl: server.url}).text(), 'rainbow'); - t.is(await ky('api/unicorn', {prefixUrl: new URL(server.url)}).text(), 'rainbow'); - t.is(await ky('unicorn', {prefixUrl: `${server.url}/api`}).text(), 'rainbow'); - t.is(await ky('unicorn', {prefixUrl: `${server.url}/api/`}).text(), 'rainbow'); - t.is(await ky('unicorn', {prefixUrl: new URL(`${server.url}/api`)}).text(), 'rainbow'); - t.is(await ky('', {prefixUrl: server.url}).text(), 'zebra'); - t.is(await ky('', {prefixUrl: `${server.url}/`}).text(), 'zebra'); - t.is(await ky('', {prefixUrl: new URL(server.url)}).text(), 'zebra'); - - t.throws( - () => { - void ky('/unicorn', {prefixUrl: `${server.url}/api`}); - }, - { - message: '`input` must not begin with a slash when using `prefixUrl`', - }, - ); - - await server.close(); -}); diff --git a/test/start-path.ts b/test/start-path.ts new file mode 100644 index 00000000..424306e2 --- /dev/null +++ b/test/start-path.ts @@ -0,0 +1,39 @@ +import test from 'ava'; +import ky from '../source/index.js'; +import {createHttpTestServer} from './helpers/create-http-test-server.js'; + +test('startPath option', async t => { + const server = await createHttpTestServer(); + server.get('/', (_request, response) => { + response.end('/'); + }); + server.get('/foo', (_request, response) => { + response.end('/foo'); + }); + server.get('/bar', (_request, response) => { + response.end('/bar'); + }); + server.get('/foo/bar', (_request, response) => { + response.end('/foo/bar'); + }); + + t.is( + // @ts-expect-error {startPath: boolean} isn't officially supported + await ky(`${server.url}/foo/bar`, {startPath: false}).text(), + '/foo/bar', + ); + t.is(await ky(`${server.url}/foo/bar`, {startPath: ''}).text(), '/foo/bar'); + t.is(await ky(new URL(`${server.url}/foo/bar`), {startPath: ''}).text(), '/foo/bar'); + t.is(await ky('foo/bar', {startPath: server.url}).text(), '/foo/bar'); + t.is(await ky('foo/bar', {startPath: new URL(server.url)}).text(), '/foo/bar'); + t.is(await ky('/bar', {startPath: `${server.url}/foo/`}).text(), '/foo/bar'); + t.is(await ky('/bar', {startPath: `${server.url}/foo`}).text(), '/foo/bar'); + t.is(await ky('bar', {startPath: `${server.url}/foo/`}).text(), '/foo/bar'); + t.is(await ky('bar', {startPath: `${server.url}/foo`}).text(), '/foo/bar'); + t.is(await ky('bar', {startPath: new URL(`${server.url}/foo`)}).text(), '/foo/bar'); + t.is(await ky('', {startPath: server.url}).text(), '/'); + t.is(await ky('', {startPath: `${server.url}/`}).text(), '/'); + t.is(await ky('', {startPath: new URL(server.url)}).text(), '/'); + + await server.close(); +});