@adamhl8/configs 0.27.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,26 @@ Configs (`tsconfig`, `oxfmt`, `oxlint`, etc.) I use across my projects.
6
6
  nub add -D @adamhl8/configs
7
7
  ```
8
8
 
9
+ ### just
10
+
11
+ Scripts live in a shared [`just`](https://github.com/casey/just) base justfile instead of being copied into every project's `package.json`. Your `justfile`:
12
+
13
+ ```just
14
+ import "node_modules/@adamhl8/configs/dist/configs/justfile.base.just"
15
+ ```
16
+
17
+ It provides `lint`, `build`, `test`, `bump-deps`, `release`, `release-run`, and `prepare`. Run `just` to list them. `build` only runs `tsdown` if the project has a `tsdown.config.ts`, so projects without a build step can use it as-is.
18
+
19
+ The only script left in `package.json` is `prepare`, which hands off to just:
20
+
21
+ ```json
22
+ "scripts": {
23
+ "prepare": "just prepare"
24
+ }
25
+ ```
26
+
27
+ The base justfile puts `node_modules/.bin` on `PATH`, so recipes can call package bins directly, just like `package.json` scripts.
28
+
9
29
  ### tsconfig
10
30
 
11
31
  ```json
@@ -98,7 +118,7 @@ adamhl8-cliff --bumped-version
98
118
 
99
119
  ### lefthook
100
120
 
101
- `lefthook.base.yaml` runs `lint` on pre-commit and `commitlint` on commit-msg. Extend it from your `lefthook.yaml`:
121
+ `lefthook.base.yaml` runs `just lint` on pre-commit and `commitlint` on commit-msg. Extend it from your `lefthook.yaml`:
102
122
 
103
123
  ```yaml
104
124
  # lefthook.yaml
@@ -106,13 +126,13 @@ extends:
106
126
  - node_modules/@adamhl8/configs/dist/configs/lefthook.base.yaml
107
127
  ```
108
128
 
109
- `lefthook install` (run it from `prepare`) sets up the hooks.
129
+ `lefthook install` (the base justfile's `prepare` recipe runs it) sets up the hooks.
110
130
 
111
131
  ## GitHub Actions
112
132
 
113
- This repo hosts three reusable workflows you can call from other projects instead of copy/pasting them. The calling project must use `nub` and define `lint` / `bundle` / `bump-deps` / `release:run` scripts and a commitlint config (e.g. via these configs).
133
+ This repo hosts three reusable workflows you can call from other projects instead of copy/pasting them. The calling project must use `nub`, have a `justfile` that imports the base justfile (see [just](#just)), and have a commitlint config (e.g. via these configs). The workflows install just themselves.
114
134
 
115
- - `ci.yml`: runs `bundle` (build + lint) on pushes/PRs and lints commit messages with commitlint
135
+ - `ci.yml`: runs `build` (build + lint) on pushes/PRs and lints commit messages with commitlint
116
136
  - `update-deps.yml`: weekly `bump-deps` run that opens a dependency-update PR
117
137
  - `release.yml`: manually dispatched release where git-cliff sets the version, then publishes to npm and cuts a GitHub release (commit and tag signed)
118
138
 
@@ -151,9 +171,9 @@ jobs:
151
171
 
152
172
  ### Release
153
173
 
154
- Manually dispatched. git-cliff computes the next version from the commits, then `release-it` bumps `package.json`, regenerates the changelog, commits and tags (both SSH-signed), publishes to npm, and cuts a GitHub release. There is no automatic trigger, so run it when a release is ready (the `release` script just does `gh workflow run release.yml`). A dispatch with no releasable commits fails fast at release-it's `Version not changed`.
174
+ Manually dispatched. git-cliff computes the next version from the commits, then `release-it` bumps `package.json`, regenerates the changelog, commits and tags (both SSH-signed), publishes to npm, and cuts a GitHub release. There is no automatic trigger, so run it when a release is ready (the `release` recipe just does `gh workflow run release.yml`). A dispatch with no releasable commits fails fast at release-it's `Version not changed`.
155
175
 
156
- Needs the `NPM_CI_TOKEN` and `CI_SIGNING_KEY` secrets (see [Secrets](#secrets)). The calling project must define `release` and `release:run` scripts, where `release:run` is `release-it --ci -i "$(adamhl8-cliff --bumped-version)"`.
176
+ Needs the `NPM_CI_TOKEN` and `CI_SIGNING_KEY` secrets (see [Secrets](#secrets)). The workflow runs the base justfile's `release-run` recipe: `release-it --ci -i "$(adamhl8-cliff --bumped-version)"`.
157
177
 
158
178
  ```yaml
159
179
  # .github/workflows/release.yml
@@ -182,6 +202,6 @@ Configure these on each repo that uses the workflows (Settings -> Secrets and va
182
202
 
183
203
  ## Notes
184
204
 
185
- The `prepare` script looks like: `"prepare": "lefthook install && tsdown"`
205
+ The `prepare` script is `"prepare": "just prepare"`, which runs the base justfile's `prepare` recipe (`lefthook install`).
186
206
 
187
- nub installs `package.json` bin executables into the local `node_modules/.bin`. This means that we can run something like `adamhl8-knip` directly instead of doing `src/adamhl8-knip/index.tx`. That's why we have `tsdown` in the prepare script. Those executables need to be available.
207
+ nub installs `package.json` bin executables into the local `node_modules/.bin`. This means that we can run something like `adamhl8-cliff` directly instead of doing `src/adamhl8-cliff/index.ts`. That's why this repo overrides the `prepare` recipe to also run `tsdown`. Those executables need to be available.
@@ -0,0 +1,42 @@
1
+ # user-defined functions are unstable as of just 1.52
2
+ set unstable
3
+ set default-list
4
+ set allow-duplicate-recipes
5
+ set lazy
6
+
7
+ export PATH := justfile_directory() + "/node_modules/.bin:" + env("PATH")
8
+
9
+ # runs cmd if the [ ] condition holds, echoing cmd only when it runs
10
+ run-when(cmd, condition) := f"if [ {{condition}} ]; then echo {{style("command", cmd)}}; {{cmd}}; fi"
11
+
12
+ current_repo := `basename -s .git $(git remote get-url origin)`
13
+
14
+ prepare:
15
+ lefthook install
16
+ @{{ run-when("tsdown", f"{{current_repo}} = configs") }}
17
+
18
+ bump-deps:
19
+ nub update -L
20
+ rm -f lock.yaml
21
+ nub i --no-frozen-lockfile
22
+ oxfmt lock.yaml
23
+
24
+ lint:
25
+ markdown-toc -i --bullets '-' --maxdepth 3 ./README.md
26
+ oxlint --fix --fix-suggestions --fix-dangerously
27
+ oxfmt
28
+ knip
29
+ @{{ run-when("just test", "-f vitest.config.ts") }}
30
+
31
+ build: lint
32
+ @{{ run-when("tsdown", "-f tsdown.config.ts") }}
33
+
34
+ test:
35
+ vitest run
36
+
37
+ release:
38
+ gh workflow run release.yml
39
+
40
+ [private]
41
+ release-run:
42
+ release-it --ci -i "$(adamhl8-cliff --bumped-version)"
@@ -5,8 +5,8 @@ import { KnipConfig } from "knip";
5
5
  declare const knipConfig: OptionalMergeConfigFn<KnipConfig, {
6
6
  readonly project: ["**/*"];
7
7
  readonly entry: ["./src/index.ts"];
8
- readonly ignoreBinaries: ["lefthook", "gh"];
9
- readonly ignoreDependencies: ["@adamhl8/eslint-plugin-clean-modules"];
8
+ readonly ignoreBinaries: ["just"];
9
+ readonly ignoreDependencies: ["@adamhl8/eslint-plugin-clean-modules", "release-it", "markdown-toc"];
10
10
  }>;
11
11
  //#endregion
12
12
  export { knipConfig };
@@ -1 +1 @@
1
- {"version":3,"file":"knip.base.d.ts","names":[],"sources":["../../src/configs/knip.base.ts"],"mappings":";;;;cAoBa,UAAA,EAAU,qBAAA,CAAA,UAAA;EAAA"}
1
+ {"version":3,"file":"knip.base.d.ts","names":[],"sources":["../../src/configs/knip.base.ts"],"mappings":";;;;cA0Ba,UAAA,EAAU,qBAAA,CAAA,UAAA;EAAA"}
@@ -1,11 +1,16 @@
1
1
  import { createMergeConfigFn } from "../utils.js";
2
2
  //#region src/configs/knip.base.ts
3
- const IGNORE_BINARIES = ["lefthook", "gh"];
3
+ /** Knip sees `just` in the `prepare` script and the release-it hooks, but it isn't a package.json dependency */
4
+ const IGNORE_BINARIES = ["just"];
4
5
  const knipConfig = createMergeConfigFn({
5
6
  project: ["**/*"],
6
7
  entry: ["./src/index.ts"],
7
8
  ignoreBinaries: IGNORE_BINARIES,
8
- ignoreDependencies: ["@adamhl8/eslint-plugin-clean-modules"]
9
+ ignoreDependencies: [
10
+ "@adamhl8/eslint-plugin-clean-modules",
11
+ "release-it",
12
+ "markdown-toc"
13
+ ]
9
14
  });
10
15
  //#endregion
11
16
  export { IGNORE_BINARIES, knipConfig };
@@ -1 +1 @@
1
- {"version":3,"file":"knip.base.js","names":[],"sources":["../../src/configs/knip.base.ts"],"sourcesContent":["import type { KnipConfig } from \"knip\"\n\nimport { createMergeConfigFn } from \"#/utils.ts\"\n\nexport const IGNORE_BINARIES = [\"lefthook\", \"gh\"] as const satisfies string[]\n\n/**\n * Consuming projects show these transitive dependencies as unlisted. e.g. knip resolves\n * `@adamhl8/eslint-plugin-clean-modules` via the oxlint config, but we don't need to install it directly in the\n * consuming project.\n */\nconst UNLISTED_DEPENDENCIES = [\"@adamhl8/eslint-plugin-clean-modules\"] as const satisfies string[]\n\nconst baseConfig = {\n project: [\"**/*\"],\n entry: [\"./src/index.ts\"],\n ignoreBinaries: IGNORE_BINARIES,\n ignoreDependencies: UNLISTED_DEPENDENCIES,\n} as const satisfies KnipConfig\n\nexport const knipConfig = createMergeConfigFn<KnipConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";;AAIA,MAAa,kBAAkB,CAAC,YAAY,IAAI;AAgBhD,MAAa,aAAa,oBAAmD;CAN3E,SAAS,CAAC,MAAM;CAChB,OAAO,CAAC,gBAAgB;CACxB,gBAAgB;CAChB,oBAAoB,CANS,sCAMW;AAGmC,CAAU"}
1
+ {"version":3,"file":"knip.base.js","names":[],"sources":["../../src/configs/knip.base.ts"],"sourcesContent":["import type { KnipConfig } from \"knip\"\n\nimport { createMergeConfigFn } from \"#/utils.ts\"\n\n/** Knip sees `just` in the `prepare` script and the release-it hooks, but it isn't a package.json dependency */\nexport const IGNORE_BINARIES = [\"just\"] as const satisfies string[]\n\n/**\n * - Knip resolves `@adamhl8/eslint-plugin-clean-modules` via the oxlint config, but we don't need to install it directly\n * in the consuming project, so it shows as unlisted\n * - `release-it` and `markdown-toc` are only referenced from the base justfile, which knip doesn't parse, so they show as\n * unused\n */\nconst IGNORE_DEPENDENCIES = [\n \"@adamhl8/eslint-plugin-clean-modules\",\n \"release-it\",\n \"markdown-toc\",\n] as const satisfies string[]\n\nconst baseConfig = {\n project: [\"**/*\"],\n entry: [\"./src/index.ts\"],\n ignoreBinaries: IGNORE_BINARIES,\n ignoreDependencies: IGNORE_DEPENDENCIES,\n} as const satisfies KnipConfig\n\nexport const knipConfig = createMergeConfigFn<KnipConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";;;AAKA,MAAa,kBAAkB,CAAC,MAAM;AAqBtC,MAAa,aAAa,oBAAmD;CAN3E,SAAS,CAAC,MAAM;CAChB,OAAO,CAAC,gBAAgB;CACxB,gBAAgB;CAChB,oBAAoB;EATpB;EACA;EACA;CAOsC;AAGqC,CAAU"}
@@ -1,7 +1,7 @@
1
1
  pre-commit:
2
2
  commands:
3
3
  lint:
4
- run: nub run lint
4
+ run: just lint
5
5
 
6
6
  commit-msg:
7
7
  commands:
@@ -4,7 +4,7 @@ import { Config } from "release-it";
4
4
  //#region src/configs/release-it.base.d.ts
5
5
  declare const releaseItConfig: OptionalMergeConfigFn<Config, {
6
6
  readonly hooks: {
7
- readonly "before:init": "nub run bundle";
7
+ readonly "before:init": "just build";
8
8
  readonly "after:bump": ["adamhl8-cliff --tag ${version} -o", "nubx oxfmt CHANGELOG.md"];
9
9
  };
10
10
  readonly git: {
@@ -1,7 +1,7 @@
1
1
  import { createMergeConfigFn } from "../utils.js";
2
2
  const releaseItConfig = createMergeConfigFn({
3
3
  hooks: {
4
- "before:init": "nub run bundle",
4
+ "before:init": "just build",
5
5
  "after:bump": ["adamhl8-cliff --tag ${version} -o", "nubx oxfmt CHANGELOG.md"]
6
6
  },
7
7
  git: {
@@ -1 +1 @@
1
- {"version":3,"file":"release-it.base.js","names":[],"sources":["../../src/configs/release-it.base.ts"],"sourcesContent":["// oxlint-disable no-template-curly-in-string\nimport type { Config } from \"release-it\"\n\nimport { createMergeConfigFn } from \"#/utils.ts\"\n\nconst baseConfig = {\n hooks: {\n \"before:init\": \"nub run bundle\",\n \"after:bump\": [\"adamhl8-cliff --tag ${version} -o\", \"nubx oxfmt CHANGELOG.md\"],\n },\n git: {\n commitMessage: \"release: ${version}\",\n tagName: \"${version}\",\n tagAnnotation: \"release ${version}\",\n addUntrackedFiles: true,\n },\n github: {\n release: true,\n releaseName: \"${version}\",\n releaseNotes: \"adamhl8-cliff --unreleased --tag ${version}\",\n },\n npm: {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion - release-it doesn't support nub directly yet\n publishPackageManager: \"nub\" as \"bun\",\n publishPath: \"\",\n publishArgs: [\"--no-git-checks\"],\n },\n} as const satisfies Config\n\nexport const releaseItConfig = createMergeConfigFn<Config, typeof baseConfig>(baseConfig)\n"],"mappings":";AA6BA,MAAa,kBAAkB,oBAA+C;CAvB5E,OAAO;EACL,eAAe;EACf,cAAc,CAAC,qCAAqC,yBAAyB;CAC/E;CACA,KAAK;EACH,eAAe;EACf,SAAS;EACT,eAAe;EACf,mBAAmB;CACrB;CACA,QAAQ;EACN,SAAS;EACT,aAAa;EACb,cAAc;CAChB;CACA,KAAK;EAEH,uBAAuB;EACvB,aAAa;EACb,aAAa,CAAC,iBAAiB;CACjC;AAG4E,CAAU"}
1
+ {"version":3,"file":"release-it.base.js","names":[],"sources":["../../src/configs/release-it.base.ts"],"sourcesContent":["// oxlint-disable no-template-curly-in-string\nimport type { Config } from \"release-it\"\n\nimport { createMergeConfigFn } from \"#/utils.ts\"\n\nconst baseConfig = {\n hooks: {\n \"before:init\": \"just build\",\n \"after:bump\": [\"adamhl8-cliff --tag ${version} -o\", \"nubx oxfmt CHANGELOG.md\"],\n },\n git: {\n commitMessage: \"release: ${version}\",\n tagName: \"${version}\",\n tagAnnotation: \"release ${version}\",\n addUntrackedFiles: true,\n },\n github: {\n release: true,\n releaseName: \"${version}\",\n releaseNotes: \"adamhl8-cliff --unreleased --tag ${version}\",\n },\n npm: {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion - release-it doesn't support nub directly yet\n publishPackageManager: \"nub\" as \"bun\",\n publishPath: \"\",\n publishArgs: [\"--no-git-checks\"],\n },\n} as const satisfies Config\n\nexport const releaseItConfig = createMergeConfigFn<Config, typeof baseConfig>(baseConfig)\n"],"mappings":";AA6BA,MAAa,kBAAkB,oBAA+C;CAvB5E,OAAO;EACL,eAAe;EACf,cAAc,CAAC,qCAAqC,yBAAyB;CAC/E;CACA,KAAK;EACH,eAAe;EACf,SAAS;EACT,eAAe;EACf,mBAAmB;CACrB;CACA,QAAQ;EACN,SAAS;EACT,aAAa;EACb,cAAc;CAChB;CACA,KAAK;EAEH,uBAAuB;EACvB,aAAa;EACb,aAAa,CAAC,iBAAiB;CACjC;AAG4E,CAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","names":[],"sources":["../src/utils.ts"],"mappings":";;;;AA6BA;;;;;;;;;UAAiB,aAAA;EAAA,iCACkB,YAAA,CAAa,UAAA,GAC5C,UAAA,EAAY,iBAAA,EACZ,OAAA;IAAW,MAAA;EAAA,IACV,SAAA,CACD,YAAA,CAAa,YAAA,CAAa,UAAA,IAC1B,YAAA,CAAa,YAAA,CAAa,iBAAA;IACxB,cAAA;EAAA;EAAA,iCAE6B,YAAA,CAAa,UAAA,GAC5C,UAAA,EAAY,iBAAA,EACZ,OAAA;IAAY,MAAA;EAAA,IACX,SAAA,CACD,YAAA,CAAa,YAAA,CAAa,UAAA,IAC1B,YAAA,CAAa,YAAA,CAAa,iBAAA;IACxB,cAAA;EAAA;AAAA;;;;;;;;UAYW,qBAAA,iCAAsD,aAAA,CAAc,UAAA,EAAY,UAAA;EAAA,IAC3F,YAAA,CAAa,YAAA,CAAa,UAAA;AAAA"}
1
+ {"version":3,"file":"utils.d.ts","names":[],"sources":["../src/utils.ts"],"mappings":";;;;AA+BA;;;;;;;;;UAAiB,aAAA;EAAA,iCACkB,YAAA,CAAa,UAAA,GAC5C,UAAA,EAAY,iBAAA,EACZ,OAAA;IAAW,MAAA;EAAA,IACV,SAAA,CACD,YAAA,CAAa,YAAA,CAAa,UAAA,IAC1B,YAAA,CAAa,YAAA,CAAa,iBAAA;IACxB,cAAA;EAAA;EAAA,iCAE6B,YAAA,CAAa,UAAA,GAC5C,UAAA,EAAY,iBAAA,EACZ,OAAA;IAAY,MAAA;EAAA,IACX,SAAA,CACD,YAAA,CAAa,YAAA,CAAa,UAAA,IAC1B,YAAA,CAAa,YAAA,CAAa,iBAAA;IACxB,cAAA;EAAA;AAAA;;;;;;;;UAYW,qBAAA,iCAAsD,aAAA,CAAc,UAAA,EAAY,UAAA;EAAA,IAC3F,YAAA,CAAa,YAAA,CAAa,UAAA;AAAA"}
package/dist/utils.js CHANGED
@@ -3,7 +3,10 @@ import { cloneDeep, mergeWith } from "es-toolkit";
3
3
  /** A wrapper around es-toolkit's `mergeWith` with a custom merge function that concatenates or replaces arrays. */
4
4
  const merge = (target, source, arrays) => mergeWith(target, source, (objValue, srcValue) => {
5
5
  if (!Array.isArray(objValue)) return;
6
- return arrays === "merge" ? objValue.concat(srcValue) : srcValue;
6
+ if (!Array.isArray(srcValue)) return srcValue;
7
+ const baseArray = objValue;
8
+ const userArray = srcValue;
9
+ return arrays === "merge" ? [...baseArray, ...userArray] : userArray;
7
10
  });
8
11
  /**
9
12
  * Creates a config merge function with proper type overloads for merging with a base config.
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { cloneDeep, mergeWith } from \"es-toolkit\"\nimport type { MergeDeep, ReadonlyDeep, WritableDeep, SimplifyDeep } from \"type-fest\"\n\ntype AnyObj = object\n\n/**\n * How arrays in the user config are combined with arrays in the base config. - `\"merge\"` (default): the user array is\n * appended to the base array - `\"replace\"`: the user array is used as-is\n */\ntype ArrayMergeMode = \"merge\" | \"replace\"\n\n/** A wrapper around es-toolkit's `mergeWith` with a custom merge function that concatenates or replaces arrays. */\nconst merge = <T extends AnyObj, S extends AnyObj>(target: T, source: S, arrays: ArrayMergeMode): T & S =>\n mergeWith(target, source, (objValue: unknown, srcValue: unknown) => {\n if (!Array.isArray(objValue)) return\n // oxlint-disable-next-line unicorn/prefer-spread\n return arrays === \"merge\" ? objValue.concat(srcValue) : srcValue\n })\n\n/**\n * The merge config function, where the `UserConfig` passed in is merged with `BaseConfig`.\n *\n * Each `arrays` mode gets its own overload so the return type's `arrayMergeMode` matches the runtime behavior. The\n * `\"replace\"` overload comes first so it wins overload resolution when `{ arrays: \"replace\" }` is passed.\n *\n * The `const` type parameter keeps literal types without requiring `as const` at the call site. The constraint is\n * `ReadonlyDeep` so readonly arrays/objects (e.g. from `as const`) are accepted, and the result is `WritableDeep` so it\n * stays assignable to the tools' own (mutable) config types.\n */\nexport interface MergeConfigFn<UserConfig, BaseConfig> {\n <const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(\n userConfig: UserConfigToMerge,\n options: { arrays: \"replace\" },\n ): MergeDeep<\n SimplifyDeep<WritableDeep<BaseConfig>>,\n SimplifyDeep<WritableDeep<UserConfigToMerge>>,\n { arrayMergeMode: \"replace\" }\n >\n <const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(\n userConfig: UserConfigToMerge,\n options?: { arrays?: \"merge\" },\n ): MergeDeep<\n SimplifyDeep<WritableDeep<BaseConfig>>,\n SimplifyDeep<WritableDeep<UserConfigToMerge>>,\n { arrayMergeMode: \"spread\" }\n >\n // Instead of returning `BaseConfig & UserConfig` (from `merge`), return a more friendly type using `MergeDeep`\n}\n\n/**\n * The optional merge config function, where the `userConfig` argument is optional. - if `UserConfig` is not provided,\n * the return type is `BaseConfig` - if `UserConfig` is provided, the return type is the merged type of `BaseConfig` and\n * `UserConfig`.\n *\n * Note that this extends `MergeConfigFn`, so this type has both function signatures on it.\n */\nexport interface OptionalMergeConfigFn<UserConfig, BaseConfig> extends MergeConfigFn<UserConfig, BaseConfig> {\n (): SimplifyDeep<WritableDeep<BaseConfig>>\n}\n\n/**\n * Creates a config merge function with proper type overloads for merging with a base config.\n *\n * `UserConfig` is not inferrable, so both type arguments must be passed explicitly. Returning the exact\n * `OptionalMergeConfigFn` instantiation (instead of relating a looser type to an annotation at the export site) keeps\n * tsc from eagerly evaluating the deep-mapped types over the full config type, which blows the instantiation depth\n * limit on large configs like tsdown's.\n */\nexport const createMergeConfigFn = <UserConfig extends AnyObj, BaseConfig extends ReadonlyDeep<UserConfig>>(\n baseConfig: BaseConfig,\n): OptionalMergeConfigFn<UserConfig, BaseConfig> => {\n // We don't care about the specific type of userConfig here because we assert mergeConfigFn as the correct type below\n const mergeConfigFn = (userConfig?: AnyObj, options?: { arrays?: ArrayMergeMode }) => {\n // Return a clone so consumers can't mutate the shared base config\n // oxlint-disable-next-line unicorn/prefer-structured-clone - structuredClone does not properly clone functions\n if (userConfig === undefined) return cloneDeep(baseConfig)\n // Clone both target and source so we never mutate the original objects\n // oxlint-disable-next-line unicorn/prefer-structured-clone - structuredClone does not properly clone functions\n return merge(cloneDeep(baseConfig), cloneDeep(userConfig), options?.arrays ?? \"merge\")\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion\n return mergeConfigFn as OptionalMergeConfigFn<UserConfig, BaseConfig>\n}\n"],"mappings":";;;AAYA,MAAM,SAA6C,QAAW,QAAW,WACvE,UAAU,QAAQ,SAAS,UAAmB,aAAsB;CAClE,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;CAE9B,OAAO,WAAW,UAAU,SAAS,OAAO,QAAQ,IAAI;AAC1D,CAAC;;;;;;;;;AAmDH,MAAa,uBACX,eACkD;CAElD,MAAM,iBAAiB,YAAqB,YAA0C;EAGpF,IAAI,eAAe,KAAA,GAAW,OAAO,UAAU,UAAU;EAGzD,OAAO,MAAM,UAAU,UAAU,GAAG,UAAU,UAAU,GAAG,SAAS,UAAU,OAAO;CACvF;CAGA,OAAO;AACT"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { cloneDeep, mergeWith } from \"es-toolkit\"\nimport type { MergeDeep, ReadonlyDeep, WritableDeep, SimplifyDeep } from \"type-fest\"\n\ntype AnyObj = object\n\n/**\n * How arrays in the user config are combined with arrays in the base config. - `\"merge\"` (default): the user array is\n * appended to the base array - `\"replace\"`: the user array is used as-is\n */\ntype ArrayMergeMode = \"merge\" | \"replace\"\n\n/** A wrapper around es-toolkit's `mergeWith` with a custom merge function that concatenates or replaces arrays. */\nconst merge = <T extends AnyObj, S extends AnyObj>(target: T, source: S, arrays: ArrayMergeMode): T & S =>\n mergeWith(target, source, (objValue: unknown, srcValue: unknown) => {\n if (!Array.isArray(objValue)) return\n if (!Array.isArray(srcValue)) return srcValue\n const baseArray: unknown[] = objValue\n const userArray: unknown[] = srcValue\n return arrays === \"merge\" ? [...baseArray, ...userArray] : userArray\n })\n\n/**\n * The merge config function, where the `UserConfig` passed in is merged with `BaseConfig`.\n *\n * Each `arrays` mode gets its own overload so the return type's `arrayMergeMode` matches the runtime behavior. The\n * `\"replace\"` overload comes first so it wins overload resolution when `{ arrays: \"replace\" }` is passed.\n *\n * The `const` type parameter keeps literal types without requiring `as const` at the call site. The constraint is\n * `ReadonlyDeep` so readonly arrays/objects (e.g. from `as const`) are accepted, and the result is `WritableDeep` so it\n * stays assignable to the tools' own (mutable) config types.\n */\nexport interface MergeConfigFn<UserConfig, BaseConfig> {\n <const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(\n userConfig: UserConfigToMerge,\n options: { arrays: \"replace\" },\n ): MergeDeep<\n SimplifyDeep<WritableDeep<BaseConfig>>,\n SimplifyDeep<WritableDeep<UserConfigToMerge>>,\n { arrayMergeMode: \"replace\" }\n >\n <const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(\n userConfig: UserConfigToMerge,\n options?: { arrays?: \"merge\" },\n ): MergeDeep<\n SimplifyDeep<WritableDeep<BaseConfig>>,\n SimplifyDeep<WritableDeep<UserConfigToMerge>>,\n { arrayMergeMode: \"spread\" }\n >\n // Instead of returning `BaseConfig & UserConfig` (from `merge`), return a more friendly type using `MergeDeep`\n}\n\n/**\n * The optional merge config function, where the `userConfig` argument is optional. - if `UserConfig` is not provided,\n * the return type is `BaseConfig` - if `UserConfig` is provided, the return type is the merged type of `BaseConfig` and\n * `UserConfig`.\n *\n * Note that this extends `MergeConfigFn`, so this type has both function signatures on it.\n */\nexport interface OptionalMergeConfigFn<UserConfig, BaseConfig> extends MergeConfigFn<UserConfig, BaseConfig> {\n (): SimplifyDeep<WritableDeep<BaseConfig>>\n}\n\n/**\n * Creates a config merge function with proper type overloads for merging with a base config.\n *\n * `UserConfig` is not inferrable, so both type arguments must be passed explicitly. Returning the exact\n * `OptionalMergeConfigFn` instantiation (instead of relating a looser type to an annotation at the export site) keeps\n * tsc from eagerly evaluating the deep-mapped types over the full config type, which blows the instantiation depth\n * limit on large configs like tsdown's.\n */\nexport const createMergeConfigFn = <UserConfig extends AnyObj, BaseConfig extends ReadonlyDeep<UserConfig>>(\n baseConfig: BaseConfig,\n): OptionalMergeConfigFn<UserConfig, BaseConfig> => {\n // We don't care about the specific type of userConfig here because we assert mergeConfigFn as the correct type below\n const mergeConfigFn = (userConfig?: AnyObj, options?: { arrays?: ArrayMergeMode }) => {\n // Return a clone so consumers can't mutate the shared base config\n // oxlint-disable-next-line unicorn/prefer-structured-clone - structuredClone does not properly clone functions\n if (userConfig === undefined) return cloneDeep(baseConfig)\n // Clone both target and source so we never mutate the original objects\n // oxlint-disable-next-line unicorn/prefer-structured-clone - structuredClone does not properly clone functions\n return merge(cloneDeep(baseConfig), cloneDeep(userConfig), options?.arrays ?? \"merge\")\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion\n return mergeConfigFn as OptionalMergeConfigFn<UserConfig, BaseConfig>\n}\n"],"mappings":";;;AAYA,MAAM,SAA6C,QAAW,QAAW,WACvE,UAAU,QAAQ,SAAS,UAAmB,aAAsB;CAClE,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;CAC9B,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACrC,MAAM,YAAuB;CAC7B,MAAM,YAAuB;CAC7B,OAAO,WAAW,UAAU,CAAC,GAAG,WAAW,GAAG,SAAS,IAAI;AAC7D,CAAC;;;;;;;;;AAmDH,MAAa,uBACX,eACkD;CAElD,MAAM,iBAAiB,YAAqB,YAA0C;EAGpF,IAAI,eAAe,KAAA,GAAW,OAAO,UAAU,UAAU;EAGzD,OAAO,MAAM,UAAU,UAAU,GAAG,UAAU,UAAU,GAAG,SAAS,UAAU,OAAO;CACvF;CAGA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adamhl8/configs",
3
- "version": "0.27.2",
3
+ "version": "0.28.0",
4
4
  "homepage": "https://github.com/adamhl8/configs",
5
5
  "bugs": {
6
6
  "url": "https://github.com/adamhl8/configs/issues"
@@ -40,12 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "scripts": {
43
- "lint": "markdown-toc -i --bullets '-' --maxdepth 3 ./README.md && oxfmt && oxlint --fix --fix-suggestions --fix-dangerously && knip",
44
- "bump-deps": "nub update -L && rm -f lock.yaml && nub i --no-frozen-lockfile && oxfmt lock.yaml",
45
- "bundle": "nub run lint && tsdown",
46
- "release": "gh workflow run release.yml",
47
- "release:run": "release-it --ci -i \"$(adamhl8-cliff --bumped-version)\"",
48
- "prepare": "lefthook install && tsdown"
43
+ "prepare": "just prepare"
49
44
  },
50
45
  "dependencies": {
51
46
  "@adamhl8/eslint-plugin-clean-modules": "^0.1.3",
@@ -77,6 +72,7 @@
77
72
  "@commitlint/cli": ">=21",
78
73
  "@commitlint/config-conventional": ">=21",
79
74
  "knip": ">=6",
75
+ "markdown-toc": ">=1",
80
76
  "oxfmt": ">=0.50",
81
77
  "oxlint": ">=1",
82
78
  "oxlint-tsgolint": ">=0.24",
@@ -99,6 +95,9 @@
99
95
  "knip": {
100
96
  "optional": true
101
97
  },
98
+ "markdown-toc": {
99
+ "optional": true
100
+ },
102
101
  "oxfmt": {
103
102
  "optional": true
104
103
  },