Skip to content

feat: deepmerge by default #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,14 @@ const { config, files } = await octokit.config.get({
<th><code>defaults</code></th>
<td>String</td>
<td>
Default options that are returned if the configuration file does not exist, or merged with the contents if it does exist. Defaults are merged shallowly using <code>Object.assign</code>. For custom merge strategies, you can set <code>defaults</code> to a function, see <a href="#custom-configuration-merging">Merging configuration</a> below for more information. Defaults to <code>{}</code>.
Default values that are returned if the configuration file does not exist. Defaults to <code>{}</code>.
</td>
</tr>
<tr>
<th><code>merge</code></th>
<td>Function</td>
<td>
Configurations are by default deepmerged using `@fastify/deepmerge`. For custom merge strategies, you can set <code>merge</code> to a function, see <a href="#custom-configuration-merging">Merging configuration</a> below for more information.
</td>
</tr>
<tr>
Expand Down Expand Up @@ -198,28 +205,18 @@ const { config } = await octokit.config.get({

The resulting `config` object is

```js
{
settings: {
one: "value from configuration";
}
}
```

And not as you might expect

```js
```json
{
settings: {
one: "value from configuration";
two: "default value";
"settings": {
"one": "value from configuration",
"two": "default value"
}
}
```

The reason for that behavior is that merging objects deeply is not supported in JavaScript by default, and there are different strategies and many pitfals. There are many libraries that support deep merging in different ways, but instead making that decision for and significantly increasing the bundle size of this plugin, we let you pass a custom merge strategy instead.
`octokit-plugin-config` merges the configurations with `@fastify/deepmerge`. If you want to use a different merge strategy, you can pass a custom merge function as the `merge` option.

In order to achive the deeply merged configuration, the `defaults` option can be set to a function. The function receives one `configs` argument, which is an array of configurations loaded from files in reverse order, so that the latter items should take precedence over the former items. The `configs` array can have more than one object if [the `_extends` key](#extends) is used.
The function receives one or many `configs` parameters, which are configurations loaded from files in reverse order, so that the latter items should take precedence over the former items. The `configs` array can have more than one object if [the `_extends` key](#extends) is used.

```js
const defaults = {
Expand All @@ -232,8 +229,9 @@ const { config } = await octokit.config.get({
owner,
repo,
path: ".github/test.yml",
defaults(configs) {
const allConfigs = [defaults, ...configs];
defaults,
merge(configs) {
const allConfigs = [...configs];
const fileSettingsConfigs = allConfigs.map(
(config: Configuration) => config.settings
);
Expand All @@ -244,14 +242,15 @@ const { config } = await octokit.config.get({
});
```

Or simpler, using a library such as [deepmerge](https://github.com/TehShrike/deepmerge)
Or simpler, if you want to only shallow copy the settings:

```js
const { config } = await octokit.config.get({
owner,
repo,
path: ".github/test.yml",
defaults: (configs) => deepmerge.all([defaults, ...configs]),
defaults,
merge: Object.assign,
});
```

Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"access": "public"
},
"dependencies": {
"@fastify/deepmerge": "^2.0.0",
"js-yaml": "^4.1.0"
},
"engines": {
Expand Down
24 changes: 18 additions & 6 deletions src/compose-config-get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { Octokit } from "@octokit/core";
import type { Configuration, GetOptions, GetResult } from "./types.js";
import type {
Configuration,
GetOptions,
GetResult,
mergeFunction,
} from "./types.js";
import { getConfigFiles } from "./util/get-config-files.js";
import { deepmerge } from "@fastify/deepmerge";

const defaultMerge = deepmerge({ all: true });

/**
* Loads configuration from one or multiple files and resolves with
Expand All @@ -12,7 +20,14 @@ import { getConfigFiles } from "./util/get-config-files.js";
*/
export async function composeConfigGet<T extends Configuration = Configuration>(
octokit: Octokit,
{ owner, repo, defaults, path, branch }: GetOptions<T>,
{
owner,
repo,
defaults = {} as T,
merge = defaultMerge as mergeFunction<T>,
path,
branch,
}: GetOptions<T>,
): Promise<GetResult<T>> {
const files = await getConfigFiles(octokit, {
owner,
Expand All @@ -28,9 +43,6 @@ export async function composeConfigGet<T extends Configuration = Configuration>(

return {
files,
config:
typeof defaults === "function"
? defaults(configs)
: Object.assign({}, defaults, ...configs),
config: merge(defaults, ...(configs as Configuration[])) as T,
};
}
13 changes: 11 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ export type GetOptions<T> = {
* An object will be merged shallowly. Pass a function for deep merges and custom merge strategies,
* @see https://github.com/probot/octokit-plugin-config/#merging-configuration
*/
defaults?: T | defaultsFunction<T>;
defaults?: T;

/**
* Custom merge function to combine multiple configurations.
*/
merge?: mergeFunction<T>;

/**
* Branch to load configuration from
*/
branch?: string;
};

Expand Down Expand Up @@ -65,4 +74,4 @@ export type ConfigFile = {
config: Configuration | null;
};

export type defaultsFunction<T> = (files: Configuration[]) => T;
export type mergeFunction<T> = (...configs: Configuration[]) => T;
96 changes: 96 additions & 0 deletions test/__snapshots__/get.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,102 @@ exports[`octokit.config.get > _extends: base:test.yml > result 1`] = `
}
`;

exports[`octokit.config.get > _extends: change to shallow copy with Object.assign > result 1`] = `
{
"config": {
"repository": {
"allow_squash_merge": false,
},
},
"files": [
{
"config": {
"repository": {
"allow_squash_merge": false,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "repo-c",
"url": "https://api.github.com/repos/org/repo-c/contents/.github%2Fsettings.yml",
},
{
"config": {
"repository": {
"allow_rebase_merge": false,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "repo-b",
"url": "https://api.github.com/repos/org/repo-b/contents/.github%2Fsettings.yml",
},
{
"config": {
"repository": {
"allow_merge_commit": true,
"allow_rebase_merge": true,
"allow_squash_merge": true,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "github-settings",
"url": "https://api.github.com/repos/org/github-settings/contents/.github%2Fsettings.yml",
},
],
}
`;

exports[`octokit.config.get > _extends: deepmerge > result 1`] = `
{
"config": {
"repository": {
"allow_merge_commit": true,
"allow_rebase_merge": false,
"allow_squash_merge": false,
},
},
"files": [
{
"config": {
"repository": {
"allow_squash_merge": false,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "repo-c",
"url": "https://api.github.com/repos/org/repo-c/contents/.github%2Fsettings.yml",
},
{
"config": {
"repository": {
"allow_rebase_merge": false,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "repo-b",
"url": "https://api.github.com/repos/org/repo-b/contents/.github%2Fsettings.yml",
},
{
"config": {
"repository": {
"allow_merge_commit": true,
"allow_rebase_merge": true,
"allow_squash_merge": true,
},
},
"owner": "org",
"path": ".github/settings.yml",
"repo": "github-settings",
"url": "https://api.github.com/repos/org/github-settings/contents/.github%2Fsettings.yml",
},
],
}
`;

exports[`octokit.config.get > _extends: other-owner/base > result 1`] = `
{
"config": {
Expand Down
Loading