Skip to content

Commit

Permalink
Add support for passing a function to ky.extend() (#611)
Browse files Browse the repository at this point in the history
* feat(extend): support function options

callback function receives parent's default options to allow extended instance to refer to modify parent options

Closes #586

* test(extend): add additional function test cases

* docs(extend): add JSDoc example
  • Loading branch information
mfulton26 authored Jul 19, 2024
1 parent 8e171f5 commit 9587c9d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 21 deletions.
16 changes: 16 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,22 @@ console.log('unicorn' in response);
//=> true
```

You can also refer to parent defaults by providing a function to `.extend()`.

```js
import ky from 'ky';

const api = ky.create({prefixUrl: 'https://example.com/api'});

const usersApi = api.extend((options) => ({prefixUrl: `${options.prefixUrl}/users`}));

const response = await usersApi.get('123');
//=> 'https://example.com/api/users/123'

const response = await api.get('version');
//=> 'https://example.com/api/version'
```

### ky.create(defaultOptions)

Create a new Ky instance with complete new defaults.
Expand Down
9 changes: 8 additions & 1 deletion source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ const createInstance = (defaults?: Partial<Options>): KyInstance => {
}

ky.create = (newDefaults?: Partial<Options>) => createInstance(validateAndMerge(newDefaults));
ky.extend = (newDefaults?: Partial<Options>) => createInstance(validateAndMerge(defaults, newDefaults));
ky.extend = (newDefaults?: Partial<Options> | ((parentDefaults: Partial<Options>) => Partial<Options>)) => {
if (typeof newDefaults === 'function') {
newDefaults = newDefaults(defaults ?? {});
}

return createInstance(validateAndMerge(defaults, newDefaults));
};

ky.stop = stop;

return ky as KyInstance;
Expand Down
19 changes: 18 additions & 1 deletion source/types/ky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,26 @@ export type KyInstance = {
In contrast to `ky.create()`, `ky.extend()` inherits defaults from its parent.
You can also refer to parent defaults by providing a function to `.extend()`.
@example
```js
import ky from 'ky';
const api = ky.create({prefixUrl: 'https://example.com/api'});
const usersApi = api.extend((options) => ({prefixUrl: `${options.prefixUrl}/users`}));
const response = await usersApi.get('123');
//=> 'https://example.com/api/users/123'
const response = await api.get('version');
//=> 'https://example.com/api/version'
```
@returns A new Ky instance.
*/
extend: (defaultOptions: Options) => KyInstance;
extend: (defaultOptions: Options | ((parentOptions: Options) => Options)) => KyInstance;

/**
A `Symbol` that can be returned by a `beforeRetry` hook to stop the retry. This will also short circuit the remaining `beforeRetry` hooks.
Expand Down
95 changes: 76 additions & 19 deletions test/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ test('ky.create() does not mangle search params', async t => {
await server.close();
});

test('ky.extend()', async t => {
const extendHooksMacro = test.macro<[{useFunction: boolean}]>(async (t, {useFunction}) => {
const server = await createHttpTestServer();
server.get('/', (_request, response) => {
response.end();
Expand All @@ -551,25 +551,28 @@ test('ky.extend()', async t => {
let isOriginBeforeRequestTrigged = false;
let isExtendBeforeRequestTrigged = false;

const intermediateOptions = {
hooks: {
beforeRequest: [
() => {
isOriginBeforeRequestTrigged = true;
},
],
},
};
const extendedOptions = {
hooks: {
beforeRequest: [
() => {
isExtendBeforeRequestTrigged = true;
},
],
},
};

const extended = ky
.extend({
hooks: {
beforeRequest: [
() => {
isOriginBeforeRequestTrigged = true;
},
],
},
})
.extend({
hooks: {
beforeRequest: [
() => {
isExtendBeforeRequestTrigged = true;
},
],
},
});
.extend(useFunction ? () => intermediateOptions : intermediateOptions)
.extend(useFunction ? () => extendedOptions : extendedOptions);

await extended(server.url);

Expand All @@ -582,6 +585,60 @@ test('ky.extend()', async t => {
await server.close();
});

test('ky.extend() appends hooks', extendHooksMacro, {useFunction: false});

test('ky.extend() with function appends hooks', extendHooksMacro, {useFunction: false});

test('ky.extend() with function overrides primitives in parent defaults', async t => {
const server = await createHttpTestServer();
server.get('*', (request, response) => {
response.end(request.url);
});

const api = ky.create({prefixUrl: `${server.url}/api`});
const usersApi = api.extend(options => ({prefixUrl: `${options.prefixUrl!.toString()}/users`}));

t.is(await usersApi.get('123').text(), '/api/users/123');
t.is(await api.get('version').text(), '/api/version');

{
const {ok} = await api.head(server.url);
t.true(ok);
}

{
const {ok} = await usersApi.head(server.url);
t.true(ok);
}

await server.close();
});

test('ky.extend() with function retains parent defaults when not specified', async t => {
const server = await createHttpTestServer();
server.get('*', (request, response) => {
response.end(request.url);
});

const api = ky.create({prefixUrl: `${server.url}/api`});
const extendedApi = api.extend(() => ({}));

t.is(await api.get('version').text(), '/api/version');
t.is(await extendedApi.get('something').text(), '/api/something');

{
const {ok} = await api.head(server.url);
t.true(ok);
}

{
const {ok} = await extendedApi.head(server.url);
t.true(ok);
}

await server.close();
});

test('throws DOMException/Error with name AbortError when aborted by user', async t => {
const server = await createHttpTestServer();
// eslint-disable-next-line @typescript-eslint/no-empty-function
Expand Down

0 comments on commit 9587c9d

Please sign in to comment.