diff --git a/README.md b/README.md
index dcda365..afecc50 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,14 @@ const { config, files } = await octokit.config.get({
defaults |
String |
- 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 Object.assign . For custom merge strategies, you can set defaults to a function, see Merging configuration below for more information. Defaults to {} .
+ Default values that are returned if the configuration file does not exist. Defaults to {} .
+ |
+
+
+ merge |
+ Function |
+
+ Configurations are by default deepmerged using `@fastify/deepmerge`. For custom merge strategies, you can set merge to a function, see Merging configuration below for more information.
|
@@ -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 = {
@@ -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
);
@@ -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,
});
```
diff --git a/package-lock.json b/package-lock.json
index 6250d15..8a56a6c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0-development",
"license": "MIT",
"dependencies": {
+ "@fastify/deepmerge": "^2.0.0",
"js-yaml": "^4.1.0"
},
"devDependencies": {
@@ -510,6 +511,11 @@
"node": ">=18"
}
},
+ "node_modules/@fastify/deepmerge": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.0.tgz",
+ "integrity": "sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g=="
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
diff --git a/package.json b/package.json
index 093e463..e90b5e1 100644
--- a/package.json
+++ b/package.json
@@ -73,6 +73,7 @@
"access": "public"
},
"dependencies": {
+ "@fastify/deepmerge": "^2.0.0",
"js-yaml": "^4.1.0"
},
"engines": {
diff --git a/src/compose-config-get.ts b/src/compose-config-get.ts
index 780ee8e..fd9af04 100644
--- a/src/compose-config-get.ts
+++ b/src/compose-config-get.ts
@@ -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
@@ -12,7 +20,14 @@ import { getConfigFiles } from "./util/get-config-files.js";
*/
export async function composeConfigGet(
octokit: Octokit,
- { owner, repo, defaults, path, branch }: GetOptions,
+ {
+ owner,
+ repo,
+ defaults = {} as T,
+ merge = defaultMerge as mergeFunction,
+ path,
+ branch,
+ }: GetOptions,
): Promise> {
const files = await getConfigFiles(octokit, {
owner,
@@ -28,9 +43,6 @@ export async function composeConfigGet(
return {
files,
- config:
- typeof defaults === "function"
- ? defaults(configs)
- : Object.assign({}, defaults, ...configs),
+ config: merge(defaults, ...(configs as Configuration[])) as T,
};
}
diff --git a/src/types.ts b/src/types.ts
index 1c614ce..0867a01 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -27,7 +27,16 @@ export type GetOptions = {
* 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;
+ defaults?: T;
+
+ /**
+ * Custom merge function to combine multiple configurations.
+ */
+ merge?: mergeFunction;
+
+ /**
+ * Branch to load configuration from
+ */
branch?: string;
};
@@ -65,4 +74,4 @@ export type ConfigFile = {
config: Configuration | null;
};
-export type defaultsFunction = (files: Configuration[]) => T;
+export type mergeFunction = (...configs: Configuration[]) => T;
diff --git a/test/__snapshots__/get.test.ts.snap b/test/__snapshots__/get.test.ts.snap
index 541ec5b..4d110d7 100644
--- a/test/__snapshots__/get.test.ts.snap
+++ b/test/__snapshots__/get.test.ts.snap
@@ -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": {
diff --git a/test/get.test.ts b/test/get.test.ts
index 81677bb..55417dc 100644
--- a/test/get.test.ts
+++ b/test/get.test.ts
@@ -17,11 +17,8 @@ const NOT_FOUND_RESPONSE = {
},
};
-const deepMergeSettings = (
- defaults: Configuration,
- configs: Configuration[],
-) => {
- const allConfigs = [defaults, ...configs];
+const customMerge = (...configs: Configuration[]) => {
+ const allConfigs = configs;
const fileSettingsConfigs = allConfigs.map(
(config: Configuration) => config.settings,
);
@@ -250,7 +247,8 @@ describe("octokit.config.get", () => {
owner: "octocat",
repo: "hello-world",
path: ".github/my-app.yml",
- defaults: (configs) => deepMergeSettings(defaults, configs),
+ defaults,
+ merge: customMerge,
});
expect(result).toMatchSnapshot("result");
@@ -396,7 +394,8 @@ describe("octokit.config.get", () => {
owner: "octocat",
repo: "hello-world",
path: ".github/my-app.yml",
- defaults: (configs) => deepMergeSettings(defaults, configs),
+ defaults,
+ merge: customMerge,
});
expect(result).toMatchSnapshot("result");
@@ -964,4 +963,105 @@ describe("octokit.config.get", () => {
expect(mock.done()).toBe(true);
});
+
+ it("_extends: deepmerge", async () => {
+ const mock = fetchMock
+ .sandbox()
+ .get(
+ "https://api.github.com/repos/org/github-settings/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ repository:
+ allow_squash_merge: true
+ allow_merge_commit: true
+ allow_rebase_merge: true
+ `),
+ )
+ .get(
+ "https://api.github.com/repos/org/repo-b/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ _extends: github-settings
+ repository:
+ allow_rebase_merge: false
+ `),
+ )
+ .get(
+ "https://api.github.com/repos/org/repo-c/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ _extends: repo-b
+ repository:
+ allow_squash_merge: false
+ `),
+ );
+
+ const octokit = new TestOctokit({
+ request: {
+ fetch: mock,
+ },
+ });
+ const result = await octokit.config.get({
+ owner: "org",
+ repo: "repo-c",
+ path: ".github/settings.yml",
+ });
+
+ expect(result.config).toEqual({
+ repository: {
+ allow_squash_merge: false,
+ allow_merge_commit: true,
+ allow_rebase_merge: false,
+ },
+ });
+ expect(result).toMatchSnapshot("result");
+ expect(mock.done()).toBe(true);
+ });
+
+ it("_extends: change to shallow copy with Object.assign", async () => {
+ const mock = fetchMock
+ .sandbox()
+ .get(
+ "https://api.github.com/repos/org/github-settings/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ repository:
+ allow_squash_merge: true
+ allow_merge_commit: true
+ allow_rebase_merge: true
+ `),
+ )
+ .get(
+ "https://api.github.com/repos/org/repo-b/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ _extends: github-settings
+ repository:
+ allow_rebase_merge: false
+ `),
+ )
+ .get(
+ "https://api.github.com/repos/org/repo-c/contents/.github%2Fsettings.yml",
+ stripIndent(`
+ _extends: repo-b
+ repository:
+ allow_squash_merge: false
+ `),
+ );
+
+ const octokit = new TestOctokit({
+ request: {
+ fetch: mock,
+ },
+ });
+ const result = await octokit.config.get({
+ owner: "org",
+ repo: "repo-c",
+ path: ".github/settings.yml",
+ merge: Object.assign,
+ });
+
+ expect(result.config).toEqual({
+ repository: {
+ allow_squash_merge: false,
+ },
+ });
+ expect(result).toMatchSnapshot("result");
+ expect(mock.done()).toBe(true);
+ });
});