@adamhl8/configs 2.1.0 → 2.2.1
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 +62 -16
- package/dist/configs/commitlint.base.d.ts +1 -1
- package/dist/configs/commitlint.base.js +1 -1
- package/dist/configs/commitlint.base.js.map +1 -1
- package/dist/configs/knip.base.d.ts +1 -1
- package/dist/configs/knip.base.js +1 -1
- package/dist/configs/knip.base.js.map +1 -1
- package/dist/configs/oxfmt.base.d.ts +1 -1
- package/dist/configs/oxfmt.base.js +1 -1
- package/dist/configs/oxfmt.base.js.map +1 -1
- package/dist/configs/oxlint.base.d.ts +1 -1
- package/dist/configs/oxlint.base.js +1 -1
- package/dist/configs/oxlint.base.js.map +1 -1
- package/dist/configs/release-it.base.d.ts +1 -1
- package/dist/configs/release-it.base.js +1 -1
- package/dist/configs/release-it.base.js.map +1 -1
- package/dist/configs/tsdown.base.d.ts +4 -1
- package/dist/configs/tsdown.base.d.ts.map +1 -1
- package/dist/configs/tsdown.base.js +3 -2
- package/dist/configs/tsdown.base.js.map +1 -1
- package/dist/merge-config/merge-config.d.ts +19 -0
- package/dist/merge-config/merge-config.d.ts.map +1 -0
- package/dist/merge-config/merge-config.js +30 -0
- package/dist/merge-config/merge-config.js.map +1 -0
- package/dist/merge-config/merge.types.d.ts +51 -0
- package/dist/merge-config/merge.types.d.ts.map +1 -0
- package/package.json +4 -4
- package/dist/utils.d.ts +0 -79
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -46
- package/dist/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,18 +8,24 @@ bun add -D @adamhl8/configs
|
|
|
8
8
|
|
|
9
9
|
### Config merging
|
|
10
10
|
|
|
11
|
-
Each `fooConfig({ ... })` function deep merges your config into the base config. Arrays are appended to the base array by default. To
|
|
11
|
+
Each `fooConfig({ ... })` function deep merges your config into the base config. Arrays are appended to the base array by default. To use arrays as-is instead, put them in a second config object, which is merged the same way except its arrays replace the base array:
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
import { oxlintConfig } from "@adamhl8/configs"
|
|
15
|
+
|
|
16
|
+
const config = oxlintConfig(
|
|
17
|
+
{
|
|
18
|
+
// appended to the base plugins
|
|
19
|
+
plugins: ["my-plugin"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
// used as-is, the base overrides are dropped
|
|
23
|
+
overrides: [],
|
|
24
|
+
},
|
|
25
|
+
)
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
The
|
|
28
|
+
The second config works at any depth: only the arrays you provide are replaced, sibling keys still merge normally, and a key in both objects gets the second object's value. Required keys (e.g. `platform` for `tsdownConfig`) must be provided in the first object.
|
|
23
29
|
|
|
24
30
|
### just
|
|
25
31
|
|
|
@@ -29,7 +35,7 @@ Scripts live in a shared [`just`](https://github.com/casey/just) base justfile i
|
|
|
29
35
|
import "node_modules/@adamhl8/configs/dist/configs/justfile.base.just"
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
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`,
|
|
38
|
+
It provides `lint`, `build`, `test`, `bump-deps`, `release`, `release-run`, `tofu-check`, and `prepare`. Run `just` to list them. The recipes degrade gracefully so projects can use them as-is: `build` only runs `tsdown` if the project has a `tsdown.config.ts`, `test` (`bun test`) passes when there are no tests, and `tofu-check` (a `tofu plan` drift check, which `build` also runs) skips itself unless run locally in a project with `.tofu` files.
|
|
33
39
|
|
|
34
40
|
To customize a recipe, redefine it in your `justfile`. Each public recipe is a thin wrapper over a private `_recipe` that holds the actual body, so an override can still run the original instead of copying it:
|
|
35
41
|
|
|
@@ -103,7 +109,7 @@ const config = tsdownConfig({ ... })
|
|
|
103
109
|
export default defineConfig(config)
|
|
104
110
|
```
|
|
105
111
|
|
|
106
|
-
`
|
|
112
|
+
`tsdownBundleConfig` is also exported for single-file bundles that aren't published libraries: everything is bundled, with no type declarations or publish checks. `tsdownBinConfig` builds `package.json` bin executables: the same bundle for node, with an extensionless output file.
|
|
107
113
|
|
|
108
114
|
### commitlint
|
|
109
115
|
|
|
@@ -168,6 +174,24 @@ dist/
|
|
|
168
174
|
|
|
169
175
|
Everything below the marker is left untouched. The sync can also be run manually by running `adamhl8-gitignore`.
|
|
170
176
|
|
|
177
|
+
### env
|
|
178
|
+
|
|
179
|
+
Utilities for validated environment variables, exported from the `/env` subpath:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { parseEnv, requireWhen } from "@adamhl8/configs/env"
|
|
183
|
+
|
|
184
|
+
const isProd = process.env["NODE_ENV"] === "production"
|
|
185
|
+
|
|
186
|
+
export const env = parseEnv({
|
|
187
|
+
API_URL: "string.url",
|
|
188
|
+
// required in production, optional otherwise
|
|
189
|
+
SENTRY_DSN: requireWhen(isProd, "string.url"),
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`parseEnv` is `arkenv` with friendlier failures: invalid env prints the validation message and exits instead of throwing. `requireWhen` makes a var required while its condition is true and optional otherwise. An optional third argument supplies a fallback for when the var is unset (the fallback is exempt from validation, a set value never is).
|
|
194
|
+
|
|
171
195
|
## GitHub Actions
|
|
172
196
|
|
|
173
197
|
This repo hosts three reusable workflows you can call from other projects instead of copy/pasting them. The calling project must use `bun`, have a `justfile` that imports the base justfile (see [just](#just)), and have a commitlint config (e.g. via these configs). The workflows set up bun and install just and lefthook themselves.
|
|
@@ -240,16 +264,38 @@ jobs:
|
|
|
240
264
|
secrets: inherit
|
|
241
265
|
```
|
|
242
266
|
|
|
267
|
+
#### Docker
|
|
268
|
+
|
|
269
|
+
Pass `docker-image` to build the repo's `Dockerfile` and push it to `ghcr.io/<owner>/<docker-image>:latest` once the release lands. The image name is separate from the repo name, since they often differ. Omit the input and no image is built.
|
|
270
|
+
|
|
271
|
+
The build checks out the tag the release just created, not `main`, so the image matches the released commit. Callers that use it must also grant `packages: write`, because a called workflow's permissions are capped by what the calling job grants.
|
|
272
|
+
|
|
273
|
+
The optional `GH_TOKEN` secret is passed to the build as a BuildKit secret named `GH_TOKEN`, for images whose build calls the GitHub API. Mount it in the Dockerfile with `RUN --mount=type=secret,id=GH_TOKEN,env=GITHUB_TOKEN`. A Dockerfile that doesn't mount it is unaffected.
|
|
274
|
+
|
|
275
|
+
To deploy after the push, add a job that needs the release. A job waiting on a called workflow waits for all of its jobs, so it runs after the image is pushed.
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
jobs:
|
|
279
|
+
release:
|
|
280
|
+
permissions:
|
|
281
|
+
contents: write
|
|
282
|
+
packages: write
|
|
283
|
+
uses: adamhl8/configs/.github/workflows/release.yml@main
|
|
284
|
+
with:
|
|
285
|
+
docker-image: actual-budget-venmo-importer
|
|
286
|
+
secrets: inherit
|
|
287
|
+
```
|
|
288
|
+
|
|
243
289
|
### Secrets
|
|
244
290
|
|
|
245
291
|
Configure these on each repo that uses the workflows (Settings -> Secrets and variables -> Actions):
|
|
246
292
|
|
|
247
|
-
| Secret | Used by
|
|
248
|
-
| ---------------- |
|
|
249
|
-
| `CI_TOKEN` | `update-deps.yml`
|
|
250
|
-
| `GH_TOKEN` | `ci.yml`
|
|
251
|
-
| `NPM_CI_TOKEN` | `release.yml`
|
|
252
|
-
| `CI_SIGNING_KEY` | `release.yml`
|
|
293
|
+
| Secret | Used by | What it is |
|
|
294
|
+
| ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
295
|
+
| `CI_TOKEN` | `update-deps.yml` | PAT with `contents` and `pull-requests` write, so the opened PR triggers CI |
|
|
296
|
+
| `GH_TOKEN` | `ci.yml`, `release.yml` | Optional. PAT for a build that calls the GitHub API. Reaches a `ci.yml` build as `GITHUB_TOKEN`, and a `release.yml` docker build as the `GH_TOKEN` BuildKit secret. Unset, the `ci.yml` build falls back to the automatic token. |
|
|
297
|
+
| `NPM_CI_TOKEN` | `release.yml` | npm automation token with publish access |
|
|
298
|
+
| `CI_SIGNING_KEY` | `release.yml` | Private SSH key that signs the release commit and tag. Add the matching public key to your GitHub account as a Signing Key so the signature verifies as you. Use a dedicated, passphrase-less key, not your personal auth key. |
|
|
253
299
|
|
|
254
300
|
`GITHUB_TOKEN` is provided automatically. The release wrapper grants it `contents: write` to push the release commit and tag and to create the GitHub release.
|
|
255
301
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OptionalMergeConfigFn } from "../
|
|
1
|
+
import { OptionalMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import { UserConfig } from "@commitlint/types";
|
|
3
3
|
//#region src/configs/commitlint.base.d.ts
|
|
4
4
|
declare const commitlintConfig: OptionalMergeConfigFn<UserConfig, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMergeConfigFn } from "../
|
|
1
|
+
import { createMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import conventional from "@commitlint/config-conventional";
|
|
3
3
|
//#region src/configs/commitlint.base.ts
|
|
4
4
|
const [_severity, _condition, conventionalTypes] = conventional.rules["type-enum"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commitlint.base.js","names":[],"sources":["../../src/configs/commitlint.base.ts"],"sourcesContent":["import conventional from \"@commitlint/config-conventional\"\nimport type { UserConfig } from \"@commitlint/types\"\n\nimport { createMergeConfigFn } from \"#
|
|
1
|
+
{"version":3,"file":"commitlint.base.js","names":[],"sources":["../../src/configs/commitlint.base.ts"],"sourcesContent":["import conventional from \"@commitlint/config-conventional\"\nimport type { UserConfig } from \"@commitlint/types\"\n\nimport { createMergeConfigFn } from \"#merge-config/merge-config.ts\"\n\nconst [_severity, _condition, conventionalTypes] = conventional.rules[\"type-enum\"]\n\nconst EXCLUDED_TYPES = new Set([\"test\", \"style\", \"build\"])\nconst filteredTypes = conventionalTypes.filter((type) => !EXCLUDED_TYPES.has(type))\nconst commitTypes = [...filteredTypes, \"improve\", \"release\"]\n\nconst baseConfig = {\n extends: [\"@commitlint/config-conventional\"],\n rules: {\n \"type-enum\": [2, \"always\", commitTypes],\n },\n} as const satisfies UserConfig\n\nexport const commitlintConfig = createMergeConfigFn<UserConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";;;AAKA,MAAM,CAAC,WAAW,YAAY,qBAAqB,aAAa,MAAM;AAEtE,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAQ;CAAS;AAAO,CAAC;AAWzD,MAAa,mBAAmB,oBAAmD;CANjF,SAAS,CAAC,iCAAiC;CAC3C,OAAO,EACL,aAAa;EAAC;EAAG;EAAU;GALV,GADC,kBAAkB,QAAQ,SAAS,CAAC,eAAe,IAAI,IAAI,CAC7C;GAAG;GAAW;EAKT;CAAC,EACxC;AAGiF,CAAU"}
|
|
@@ -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 \"#
|
|
1
|
+
{"version":3,"file":"knip.base.js","names":[],"sources":["../../src/configs/knip.base.ts"],"sourcesContent":["import type { KnipConfig } from \"knip\"\n\nimport { createMergeConfigFn } from \"#merge-config/merge-config.ts\"\n\n/** Knip sees `just` in the `prepare` script and the release-it hooks, but it isn't a package.json dependency */\nconst 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 */\nconst IGNORE_DEPENDENCIES = [\"@adamhl8/eslint-plugin-clean-modules\"] as const satisfies string[]\n\nconst baseConfig = {\n project: [\"**/*\"],\n ignoreBinaries: IGNORE_BINARIES,\n ignoreDependencies: IGNORE_DEPENDENCIES,\n bun: {\n entry: [\"**/*.test.ts\"],\n },\n} as const satisfies KnipConfig\n\nexport const knipConfig = createMergeConfigFn<KnipConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";AAsBA,MAAa,aAAa,oBAAmD;CAR3E,SAAS,CAAC,MAAM;CAChB,gBAAgB,CAVO,MAUO;CAC9B,oBAAoB,CALO,sCAKW;CACtC,KAAK,EACH,OAAO,CAAC,cAAc,EACxB;AAG2E,CAAU"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OptionalMergeConfigFn } from "../
|
|
1
|
+
import { OptionalMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import { OxfmtConfig } from "oxfmt";
|
|
3
3
|
//#region src/configs/oxfmt.base.d.ts
|
|
4
4
|
declare const oxfmtConfig: OptionalMergeConfigFn<OxfmtConfig, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oxfmt.base.js","names":[],"sources":["../../src/configs/oxfmt.base.ts"],"sourcesContent":["import type { OxfmtConfig } from \"oxfmt\"\n\nimport { createMergeConfigFn } from \"#
|
|
1
|
+
{"version":3,"file":"oxfmt.base.js","names":[],"sources":["../../src/configs/oxfmt.base.ts"],"sourcesContent":["import type { OxfmtConfig } from \"oxfmt\"\n\nimport { createMergeConfigFn } from \"#merge-config/merge-config.ts\"\n\nconst baseConfig = {\n printWidth: 120,\n semi: false,\n jsdoc: true,\n sortImports: {\n groups: [\n \"builtin\",\n \"external\",\n [\"subpath\", \"internal\"],\n [\"index\", \"sibling\", \"parent\"],\n \"side_effect\",\n [\"style\", \"side_effect_style\"],\n ],\n },\n sortTailwindcss: true,\n} as const satisfies OxfmtConfig\n\nexport const oxfmtConfig = createMergeConfigFn<OxfmtConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";AAqBA,MAAa,cAAc,oBAAoD;CAhB7E,YAAY;CACZ,MAAM;CACN,OAAO;CACP,aAAa,EACX,QAAQ;EACN;EACA;EACA,CAAC,WAAW,UAAU;EACtB;GAAC;GAAS;GAAW;EAAQ;EAC7B;EACA,CAAC,SAAS,mBAAmB;CAC/B,EACF;CACA,iBAAiB;AAG4D,CAAU"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OptionalMergeConfigFn } from "../
|
|
1
|
+
import { OptionalMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import { OxlintConfig } from "oxlint";
|
|
3
3
|
//#region src/configs/oxlint.base.d.ts
|
|
4
4
|
declare const oxlintConfig: OptionalMergeConfigFn<OxlintConfig, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oxlint.base.js","names":[],"sources":["../../src/configs/oxlint.base.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\"\n\nimport type { OxlintConfig } from \"oxlint\"\n\nimport { createMergeConfigFn } from \"#
|
|
1
|
+
{"version":3,"file":"oxlint.base.js","names":[],"sources":["../../src/configs/oxlint.base.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\"\n\nimport type { OxlintConfig } from \"oxlint\"\n\nimport { createMergeConfigFn } from \"#merge-config/merge-config.ts\"\n\nconst baseConfig = {\n options: {\n reportUnusedDisableDirectives: \"error\",\n typeAware: true,\n typeCheck: true,\n },\n\n categories: {\n correctness: \"error\",\n nursery: \"error\",\n pedantic: \"error\",\n perf: \"error\",\n restriction: \"error\",\n style: \"error\",\n suspicious: \"error\",\n },\n\n plugins: [\n \"eslint\",\n \"react\",\n \"unicorn\",\n \"typescript\",\n \"oxc\",\n \"import\",\n \"jsdoc\",\n \"vitest\",\n \"jsx-a11y\",\n \"react-perf\",\n \"promise\",\n \"node\",\n ],\n\n // Resolve to an absolute path so consumers load it from this package's own deps.\n jsPlugins: [fileURLToPath(import.meta.resolve(\"@adamhl8/eslint-plugin-clean-modules\"))],\n\n overrides: [\n {\n files: [\n \".release-it.ts\",\n \"astro.config.ts\",\n \"commitlint.config.ts\",\n \"knip.ts\",\n \"oxfmt.config.ts\",\n \"oxlint.config.ts\",\n \"prisma.config.ts\",\n \"tsdown.config.ts\",\n ],\n rules: {\n \"import/no-default-export\": \"off\",\n },\n },\n {\n files: [\".release-it.ts\"],\n rules: {\n \"no-template-curly-in-string\": \"off\",\n },\n },\n ],\n\n rules: {\n \"clean-modules/require-direct-exports\": \"error\",\n \"clean-modules/require-import-extensions\": \"error\",\n \"clean-modules/require-subpath-imports\": \"error\",\n\n curly: [\"error\", \"multi-or-nest\"],\n \"func-style\": [\"error\", \"expression\"],\n \"react/jsx-curly-brace-presence\": [\"error\", { children: \"never\", propElementValues: \"always\", props: \"never\" }],\n \"require-unicode-regexp\": [\"error\", { requireFlag: \"v\" }],\n \"unicorn/filename-case\": [\n \"error\",\n {\n cases: { kebabCase: true, pascalCase: true },\n },\n ],\n \"unicorn/no-useless-undefined\": [\"error\", { checkArguments: false }],\n\n \"capitalized-comments\": \"off\",\n \"id-length\": \"off\",\n \"import/exports-last\": \"off\",\n \"import/group-exports\": \"off\",\n \"import/max-dependencies\": \"off\",\n \"import/no-named-export\": \"off\",\n \"import/no-nodejs-modules\": \"off\",\n \"import/no-unassigned-import\": \"off\",\n \"import/prefer-default-export\": \"off\",\n \"init-declarations\": \"off\",\n \"jsdoc/require-param\": \"off\",\n \"jsdoc/require-returns\": \"off\",\n \"max-classes-per-file\": \"off\",\n \"max-lines\": \"off\",\n \"max-lines-per-function\": \"off\",\n \"max-statements\": \"off\",\n \"no-console\": \"off\",\n \"no-continue\": \"off\",\n \"no-duplicate-imports\": \"off\",\n \"no-inline-comments\": \"off\",\n \"no-magic-numbers\": \"off\",\n \"no-plusplus\": \"off\",\n \"no-ternary\": \"off\",\n \"no-undef\": \"off\",\n \"no-undefined\": \"off\",\n \"no-useless-return\": \"off\",\n \"no-void\": \"off\",\n \"node/callback-return\": \"off\",\n \"node/no-sync\": \"off\",\n \"oxc/no-async-await\": \"off\",\n \"oxc/no-optional-chaining\": \"off\",\n \"oxc/no-rest-spread-properties\": \"off\",\n \"react/forbid-component-props\": \"off\",\n \"react/jsx-filename-extension\": \"off\",\n \"react/jsx-max-depth\": \"off\",\n \"react/jsx-no-literals\": \"off\",\n \"react/react-in-jsx-scope\": \"off\",\n \"require-await\": \"off\",\n \"sort-imports\": \"off\",\n \"sort-keys\": \"off\",\n \"typescript/consistent-return\": \"off\",\n \"typescript/explicit-function-return-type\": \"off\",\n \"typescript/explicit-module-boundary-types\": \"off\",\n \"typescript/prefer-readonly-parameter-types\": \"off\",\n \"typescript/strict-boolean-expressions\": \"off\",\n \"unicorn/no-array-callback-reference\": \"off\",\n \"unicorn/no-array-method-this-argument\": \"off\",\n \"unicorn/no-await-expression-member\": \"off\",\n \"unicorn/no-process-exit\": \"off\",\n \"unicorn/number-literal-case\": \"off\",\n \"unicorn/switch-case-braces\": \"off\",\n \"vitest/no-hooks\": \"off\",\n \"vitest/no-importing-vitest-globals\": \"off\",\n \"vitest/prefer-describe-function-title\": \"off\",\n \"vitest/prefer-expect-assertions\": \"off\",\n \"vitest/prefer-importing-vitest-globals\": \"off\",\n \"vitest/prefer-lowercase-title\": \"off\",\n \"vitest/prefer-to-be-falsy\": \"off\",\n \"vitest/prefer-to-be-truthy\": \"off\",\n \"vitest/require-hook\": \"off\", // reenable when false positives are fixed?\n \"vitest/require-test-timeout\": \"off\",\n },\n} as const satisfies OxlintConfig\n\nexport const oxlintConfig = createMergeConfigFn<OxlintConfig, typeof baseConfig>(baseConfig)\n"],"mappings":";;AAkJA,MAAa,eAAe,oBAAqD;CA3I/E,SAAS;EACP,+BAA+B;EAC/B,WAAW;EACX,WAAW;CACb;CAEA,YAAY;EACV,aAAa;EACb,SAAS;EACT,UAAU;EACV,MAAM;EACN,aAAa;EACb,OAAO;EACP,YAAY;CACd;CAEA,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CAGA,WAAW,CAAC,cAAc,OAAO,KAAK,QAAQ,sCAAsC,CAAC,CAAC;CAEtF,WAAW,CACT;EACE,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EACA,OAAO,EACL,4BAA4B,MAC9B;CACF,GACA;EACE,OAAO,CAAC,gBAAgB;EACxB,OAAO,EACL,+BAA+B,MACjC;CACF,CACF;CAEA,OAAO;EACL,wCAAwC;EACxC,2CAA2C;EAC3C,yCAAyC;EAEzC,OAAO,CAAC,SAAS,eAAe;EAChC,cAAc,CAAC,SAAS,YAAY;EACpC,kCAAkC,CAAC,SAAS;GAAE,UAAU;GAAS,mBAAmB;GAAU,OAAO;EAAQ,CAAC;EAC9G,0BAA0B,CAAC,SAAS,EAAE,aAAa,IAAI,CAAC;EACxD,yBAAyB,CACvB,SACA,EACE,OAAO;GAAE,WAAW;GAAM,YAAY;EAAK,EAC7C,CACF;EACA,gCAAgC,CAAC,SAAS,EAAE,gBAAgB,MAAM,CAAC;EAEnE,wBAAwB;EACxB,aAAa;EACb,uBAAuB;EACvB,wBAAwB;EACxB,2BAA2B;EAC3B,0BAA0B;EAC1B,4BAA4B;EAC5B,+BAA+B;EAC/B,gCAAgC;EAChC,qBAAqB;EACrB,uBAAuB;EACvB,yBAAyB;EACzB,wBAAwB;EACxB,aAAa;EACb,0BAA0B;EAC1B,kBAAkB;EAClB,cAAc;EACd,eAAe;EACf,wBAAwB;EACxB,sBAAsB;EACtB,oBAAoB;EACpB,eAAe;EACf,cAAc;EACd,YAAY;EACZ,gBAAgB;EAChB,qBAAqB;EACrB,WAAW;EACX,wBAAwB;EACxB,gBAAgB;EAChB,sBAAsB;EACtB,4BAA4B;EAC5B,iCAAiC;EACjC,gCAAgC;EAChC,gCAAgC;EAChC,uBAAuB;EACvB,yBAAyB;EACzB,4BAA4B;EAC5B,iBAAiB;EACjB,gBAAgB;EAChB,aAAa;EACb,gCAAgC;EAChC,4CAA4C;EAC5C,6CAA6C;EAC7C,8CAA8C;EAC9C,yCAAyC;EACzC,uCAAuC;EACvC,yCAAyC;EACzC,sCAAsC;EACtC,2BAA2B;EAC3B,+BAA+B;EAC/B,8BAA8B;EAC9B,mBAAmB;EACnB,sCAAsC;EACtC,yCAAyC;EACzC,mCAAmC;EACnC,0CAA0C;EAC1C,iCAAiC;EACjC,6BAA6B;EAC7B,8BAA8B;EAC9B,uBAAuB;EACvB,+BAA+B;CACjC;AAG+E,CAAU"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OptionalMergeConfigFn } from "../
|
|
1
|
+
import { OptionalMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import { Config } from "release-it";
|
|
3
3
|
//#region src/configs/release-it.base.d.ts
|
|
4
4
|
declare const releaseItConfig: OptionalMergeConfigFn<Config, {
|
|
@@ -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 \"#
|
|
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 \"#merge-config/merge-config.ts\"\n\nconst baseConfig = {\n hooks: {\n \"before:init\": \"just build\",\n \"after:bump\": [\"bun adamhl8-cliff --tag ${version} -o\", \"bun 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: \"bun adamhl8-cliff --unreleased --tag ${version}\",\n },\n npm: {\n publishPackageManager: \"bun\",\n publishPath: \"\",\n publishArgs: [\"--ignore-scripts\"],\n },\n} as const satisfies Config\n\nexport const releaseItConfig = createMergeConfigFn<Config, typeof baseConfig>(baseConfig)\n"],"mappings":";AA4BA,MAAa,kBAAkB,oBAA+C;CAtB5E,OAAO;EACL,eAAe;EACf,cAAc,CAAC,yCAAyC,wBAAwB;CAClF;CACA,KAAK;EACH,eAAe;EACf,SAAS;EACT,eAAe;EACf,mBAAmB;CACrB;CACA,QAAQ;EACN,SAAS;EACT,aAAa;EACb,cAAc;CAChB;CACA,KAAK;EACH,uBAAuB;EACvB,aAAa;EACb,aAAa,CAAC,kBAAkB;CAClC;AAG4E,CAAU"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MergeConfigFn } from "../
|
|
1
|
+
import { MergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
import { SetRequired } from "type-fest";
|
|
3
3
|
import { UserConfig } from "tsdown";
|
|
4
4
|
//#region src/configs/tsdown.base.d.ts
|
|
@@ -26,6 +26,7 @@ declare const baseConfig: {
|
|
|
26
26
|
};
|
|
27
27
|
readonly publint: true;
|
|
28
28
|
readonly failOnWarn: true;
|
|
29
|
+
readonly suppressWarnings: ["does not yet have a stable API"];
|
|
29
30
|
};
|
|
30
31
|
type StrictBundleConfig = SetRequired<UserConfig, "platform" | "entry">;
|
|
31
32
|
declare const bundleConfig: {
|
|
@@ -38,6 +39,7 @@ declare const bundleConfig: {
|
|
|
38
39
|
readonly neverBundle: ["bun"];
|
|
39
40
|
};
|
|
40
41
|
readonly failOnWarn: true;
|
|
42
|
+
readonly suppressWarnings: ["does not yet have a stable API"];
|
|
41
43
|
readonly entry: [];
|
|
42
44
|
readonly unbundle: false;
|
|
43
45
|
readonly sourcemap: false;
|
|
@@ -60,6 +62,7 @@ declare const binConfig: {
|
|
|
60
62
|
readonly neverBundle: ["bun"];
|
|
61
63
|
};
|
|
62
64
|
readonly failOnWarn: true;
|
|
65
|
+
readonly suppressWarnings: ["does not yet have a stable API"];
|
|
63
66
|
readonly entry: [];
|
|
64
67
|
readonly unbundle: false;
|
|
65
68
|
readonly sourcemap: false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tsdown.base.d.ts","names":[],"sources":["../../src/configs/tsdown.base.ts"],"mappings":";;;;KAOK,eAAe,YAAY;cAE1B;;;;;;;;;;aAUF;aACA;aACA;;;aAGA;;;aAGA;aACA
|
|
1
|
+
{"version":3,"file":"tsdown.base.d.ts","names":[],"sources":["../../src/configs/tsdown.base.ts"],"mappings":";;;;KAOK,eAAe,YAAY;cAE1B;;;;;;;;;;aAUF;aACA;aACA;;;aAGA;;;aAGA;aACA;;;;;;KAQC,qBAAqB,YAAY;cAGhC;;;;;;;aAfF;;;;;;;;;;;aAwBA;eACE;;;;KAMD,kBAAkB,YAAY;cAG7B;;;;;;aAlCF;;;;;;;;;;;aAwBA;eACE;;;;;;;;cAiBO,cAAc,cAAc,qBAAqB;cAKjD,oBAAoB,cAAc,2BAA2B;cAK7D,iBAAiB,cAAc,wBAAwB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMergeConfigFn } from "../
|
|
1
|
+
import { createMergeConfigFn } from "../merge-config/merge-config.js";
|
|
2
2
|
//#region src/configs/tsdown.base.ts
|
|
3
3
|
const baseConfig = {
|
|
4
4
|
entry: ["./src/index.ts"],
|
|
@@ -20,7 +20,8 @@ const baseConfig = {
|
|
|
20
20
|
profile: "esm-only"
|
|
21
21
|
},
|
|
22
22
|
publint: true,
|
|
23
|
-
failOnWarn: true
|
|
23
|
+
failOnWarn: true,
|
|
24
|
+
suppressWarnings: ["does not yet have a stable API"]
|
|
24
25
|
};
|
|
25
26
|
const bundleConfig = {
|
|
26
27
|
...baseConfig,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tsdown.base.js","names":[],"sources":["../../src/configs/tsdown.base.ts"],"sourcesContent":["import type { UserConfig } from \"tsdown\"\nimport type { SetRequired } from \"type-fest\"\n\nimport type { MergeConfigFn } from \"#
|
|
1
|
+
{"version":3,"file":"tsdown.base.js","names":[],"sources":["../../src/configs/tsdown.base.ts"],"sourcesContent":["import type { UserConfig } from \"tsdown\"\nimport type { SetRequired } from \"type-fest\"\n\nimport type { MergeConfigFn } from \"#merge-config/merge-config.ts\"\nimport { createMergeConfigFn } from \"#merge-config/merge-config.ts\"\n\n// Force projects to specify platform\ntype StrictConfig = SetRequired<UserConfig, \"platform\">\n\nconst baseConfig = {\n entry: [\"./src/index.ts\"],\n platform: \"neutral\",\n target: false,\n unbundle: true,\n sourcemap: true,\n minify: \"dce-only\",\n fixedExtension: false,\n hash: false,\n dts: {\n newContext: true,\n tsgo: true,\n sourcemap: true,\n },\n deps: {\n neverBundle: [\"bun\"],\n },\n attw: {\n level: \"error\",\n profile: \"esm-only\",\n },\n publint: true,\n failOnWarn: true,\n suppressWarnings: [\"does not yet have a stable API\"],\n} as const satisfies StrictConfig\n\n// Force projects to specify platform and entry\ntype StrictBundleConfig = SetRequired<UserConfig, \"platform\" | \"entry\">\n\n// Single-file bundle (not a published library): bundle everything, drop types/lint/sourcemap.\nconst bundleConfig = {\n ...baseConfig,\n entry: [],\n unbundle: false,\n sourcemap: false,\n dts: false,\n attw: false,\n publint: false,\n inputOptions: {\n experimental: {\n attachDebugInfo: \"none\", // remove region comments from bundled output\n },\n },\n} as const satisfies StrictBundleConfig\n\n// Force projects to specify platform and entry\ntype StrictBinConfig = SetRequired<UserConfig, \"entry\">\n\n// A bin is a node bundle with no output file extension.\nconst binConfig = {\n ...bundleConfig,\n platform: \"node\",\n outExtensions: () => ({ js: \"\" }) as const,\n} as const satisfies StrictBinConfig\n\n// The export annotations forbid the no-arg call (platform/entry must be provided) and are load-bearing for\n// declaration emit: without one, tsgo synthesizes the type and fails with TS2883 on tsdown's non-exported internals.\nexport const tsdownConfig: MergeConfigFn<StrictConfig, typeof baseConfig> = createMergeConfigFn<\n StrictConfig,\n typeof baseConfig\n>(baseConfig)\n\nexport const tsdownBundleConfig: MergeConfigFn<StrictBundleConfig, typeof bundleConfig> = createMergeConfigFn<\n StrictBundleConfig,\n typeof bundleConfig\n>(bundleConfig)\n\nexport const tsdownBinConfig: MergeConfigFn<StrictBinConfig, typeof binConfig> = createMergeConfigFn<\n StrictBinConfig,\n typeof binConfig\n>(binConfig)\n"],"mappings":";;AASA,MAAM,aAAa;CACjB,OAAO,CAAC,gBAAgB;CACxB,UAAU;CACV,QAAQ;CACR,UAAU;CACV,WAAW;CACX,QAAQ;CACR,gBAAgB;CAChB,MAAM;CACN,KAAK;EACH,YAAY;EACZ,MAAM;EACN,WAAW;CACb;CACA,MAAM,EACJ,aAAa,CAAC,KAAK,EACrB;CACA,MAAM;EACJ,OAAO;EACP,SAAS;CACX;CACA,SAAS;CACT,YAAY;CACZ,kBAAkB,CAAC,gCAAgC;AACrD;AAMA,MAAM,eAAe;CACnB,GAAG;CACH,OAAO,CAAC;CACR,UAAU;CACV,WAAW;CACX,KAAK;CACL,MAAM;CACN,SAAS;CACT,cAAc,EACZ,cAAc,EACZ,iBAAiB,OACnB,EACF;AACF;AAMA,MAAM,YAAY;CAChB,GAAG;CACH,UAAU;CACV,sBAAsB,EAAE,IAAI,GAAG;AACjC;AAIA,MAAa,eAA+D,oBAG1E,UAAU;AAEZ,MAAa,qBAA6E,oBAGxF,YAAY;AAEd,MAAa,kBAAoE,oBAG/E,SAAS"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Clean, MergedConfig } from "./merge.types.js";
|
|
2
|
+
import { ReadonlyDeep } from "type-fest";
|
|
3
|
+
//#region src/merge-config/merge-config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The merge config function. `userConfig` is deep merged into `BaseConfig` with arrays appended to the base array. The
|
|
6
|
+
* optional `replaceConfig` is then deep merged into that result with arrays used as-is, so putting an array there
|
|
7
|
+
* replaces just that key (sibling keys still merge normally, and a key in both configs gets the replace config's
|
|
8
|
+
* value). Required keys must be provided in `userConfig`.
|
|
9
|
+
*
|
|
10
|
+
* The `const` type parameters keep literal types without requiring `as const` at the call site. The constraints accept
|
|
11
|
+
* readonly arrays/objects (e.g. from `as const`), and the result is writable so it stays assignable to the tools' own
|
|
12
|
+
* (mutable) config types.
|
|
13
|
+
*/
|
|
14
|
+
type MergeConfigFn<UserConfig, BaseConfig> = (<const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(userConfig: UserConfigToMerge) => MergedConfig<BaseConfig, UserConfigToMerge>) & (<const UserConfigToMerge extends ReadonlyDeep<UserConfig>, const ReplaceConfigToMerge extends ReadonlyDeep<Partial<UserConfig>>>(userConfig: UserConfigToMerge, replaceConfig: ReplaceConfigToMerge) => MergedConfig<MergedConfig<BaseConfig, UserConfigToMerge>, ReplaceConfigToMerge, "replace">);
|
|
15
|
+
/** A MergeConfigFn that can also be called with no arguments, returning a writable clone of the base config. */
|
|
16
|
+
type OptionalMergeConfigFn<UserConfig, BaseConfig> = MergeConfigFn<UserConfig, BaseConfig> & (() => Clean<BaseConfig>);
|
|
17
|
+
//#endregion
|
|
18
|
+
export { MergeConfigFn, OptionalMergeConfigFn };
|
|
19
|
+
//# sourceMappingURL=merge-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-config.d.ts","names":[],"sources":["../../src/merge-config/merge-config.ts"],"mappings":";;;;;;;;;;;;;KAgCY,cAAc,YAAY,sBAAsB,0BAA0B,aAAa,aACjG,YAAY,sBACT,aAAa,YAAY,8BAEpB,0BAA0B,aAAa,mBACvC,6BAA6B,aAAa,QAAQ,cAExD,YAAY,mBACZ,eAAe,yBACZ,aAAa,aAAa,YAAY,oBAAoB;;KAGrD,sBAAsB,YAAY,cAAc,cAAc,YAAY,qBAC7E,MAAM"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cloneDeep, mergeWith } from "es-toolkit";
|
|
2
|
+
//#region src/merge-config/merge-config.ts
|
|
3
|
+
/**
|
|
4
|
+
* The runtime counterpart of MergedConfig in merge.types.ts: a deep merge whose customizer applies the array mode. The
|
|
5
|
+
* source is pre-cloned so the result never aliases the caller's objects. The merge visits string keys only, so
|
|
6
|
+
* symbol-keyed properties in user configs are dropped (the types mirror this).
|
|
7
|
+
*/
|
|
8
|
+
const merge = (target, source, mode) => mergeWith(target, cloneDeep(source), (objValue, srcValue) => {
|
|
9
|
+
if (!Array.isArray(srcValue)) return;
|
|
10
|
+
const userArray = srcValue;
|
|
11
|
+
if (mode === "spread" && Array.isArray(objValue)) return [...objValue, ...userArray];
|
|
12
|
+
return userArray;
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Creates a config merge function with proper types for merging with a base config. `UserConfig` is not inferrable, so
|
|
16
|
+
* both type arguments must be passed explicitly.
|
|
17
|
+
*/
|
|
18
|
+
const createMergeConfigFn = (baseConfig) => {
|
|
19
|
+
const mergeConfigFn = (userConfig, replaceConfig) => {
|
|
20
|
+
const result = cloneDeep(baseConfig);
|
|
21
|
+
if (userConfig !== void 0) merge(result, userConfig, "spread");
|
|
22
|
+
if (replaceConfig !== void 0) merge(result, replaceConfig, "replace");
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
return mergeConfigFn;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { createMergeConfigFn };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=merge-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-config.js","names":["baseArray"],"sources":["../../src/merge-config/merge-config.ts"],"sourcesContent":["/* oxlint-disable unicorn/prefer-structured-clone -- structuredClone does not properly clone functions */\nimport { cloneDeep, mergeWith } from \"es-toolkit\"\nimport type { ReadonlyDeep } from \"type-fest\"\n\nimport type { Clean, MergedConfig, Mode } from \"#merge-config/merge.types.ts\"\n\n/**\n * The runtime counterpart of MergedConfig in merge.types.ts: a deep merge whose customizer applies the array mode. The\n * source is pre-cloned so the result never aliases the caller's objects. The merge visits string keys only, so\n * symbol-keyed properties in user configs are dropped (the types mirror this).\n */\nconst merge = (target: object, source: object, mode: Mode): object =>\n mergeWith(target, cloneDeep(source), (objValue: unknown, srcValue: unknown) => {\n if (!Array.isArray(srcValue)) return\n const userArray: unknown[] = srcValue\n if (mode === \"spread\" && Array.isArray(objValue)) {\n const baseArray: unknown[] = objValue\n return [...baseArray, ...userArray]\n }\n return userArray\n })\n\n/**\n * The merge config function. `userConfig` is deep merged into `BaseConfig` with arrays appended to the base array. The\n * optional `replaceConfig` is then deep merged into that result with arrays used as-is, so putting an array there\n * replaces just that key (sibling keys still merge normally, and a key in both configs gets the replace config's\n * value). Required keys must be provided in `userConfig`.\n *\n * The `const` type parameters keep literal types without requiring `as const` at the call site. The constraints accept\n * readonly arrays/objects (e.g. from `as const`), and the result is writable so it stays assignable to the tools' own\n * (mutable) config types.\n */\nexport type MergeConfigFn<UserConfig, BaseConfig> = (<const UserConfigToMerge extends ReadonlyDeep<UserConfig>>(\n userConfig: UserConfigToMerge,\n) => MergedConfig<BaseConfig, UserConfigToMerge>) &\n (<\n const UserConfigToMerge extends ReadonlyDeep<UserConfig>,\n const ReplaceConfigToMerge extends ReadonlyDeep<Partial<UserConfig>>,\n >(\n userConfig: UserConfigToMerge,\n replaceConfig: ReplaceConfigToMerge,\n ) => MergedConfig<MergedConfig<BaseConfig, UserConfigToMerge>, ReplaceConfigToMerge, \"replace\">)\n\n/** A MergeConfigFn that can also be called with no arguments, returning a writable clone of the base config. */\nexport type OptionalMergeConfigFn<UserConfig, BaseConfig> = MergeConfigFn<UserConfig, BaseConfig> &\n (() => Clean<BaseConfig>)\n\n/**\n * Creates a config merge function with proper types for merging with a base config. `UserConfig` is not inferrable, so\n * both type arguments must be passed explicitly.\n */\nexport const createMergeConfigFn = <UserConfig extends object, BaseConfig extends ReadonlyDeep<UserConfig>>(\n baseConfig: BaseConfig,\n): OptionalMergeConfigFn<UserConfig, BaseConfig> => {\n // Loosely typed on purpose, the assertion below gives it the precise public signature\n const mergeConfigFn = (userConfig?: object, replaceConfig?: object) => {\n // Start from a clone so consumers can't mutate the shared base config\n const result = cloneDeep(baseConfig)\n if (userConfig !== undefined) merge(result, userConfig, \"spread\")\n if (replaceConfig !== undefined) merge(result, replaceConfig, \"replace\")\n return result\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion\n return mergeConfigFn as OptionalMergeConfigFn<UserConfig, BaseConfig>\n}\n"],"mappings":";;;;;;;AAWA,MAAM,SAAS,QAAgB,QAAgB,SAC7C,UAAU,QAAQ,UAAU,MAAM,IAAI,UAAmB,aAAsB;CAC7E,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;CAC9B,MAAM,YAAuB;CAC7B,IAAI,SAAS,YAAY,MAAM,QAAQ,QAAQ,GAE7C,OAAO,CAAC,GAAGA,UAAW,GAAG,SAAS;CAEpC,OAAO;AACT,CAAC;;;;;AA+BH,MAAa,uBACX,eACkD;CAElD,MAAM,iBAAiB,YAAqB,kBAA2B;EAErE,MAAM,SAAS,UAAU,UAAU;EACnC,IAAI,eAAe,KAAA,GAAW,MAAM,QAAQ,YAAY,QAAQ;EAChE,IAAI,kBAAkB,KAAA,GAAW,MAAM,QAAQ,eAAe,SAAS;EACvE,OAAO;CACT;CAGA,OAAO;AACT"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/merge-config/merge.types.d.ts
|
|
2
|
+
type AnyFunction = (...args: never[]) => unknown;
|
|
3
|
+
/** How user arrays combine with base arrays: appended in spread mode, used as-is in replace mode. */
|
|
4
|
+
type Mode = "spread" | "replace";
|
|
5
|
+
/**
|
|
6
|
+
* Recursion budget for the merge walk, threaded through every recursive type below. When a call fails the generic
|
|
7
|
+
* constraint, these types run over the full constraint (the whole tool config type, with circular types like Console in
|
|
8
|
+
* it), and the budget makes that walk terminate instead of exploding.
|
|
9
|
+
*/
|
|
10
|
+
type MaxDepth = readonly [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
11
|
+
/**
|
|
12
|
+
* Values the walk must keep as-is instead of mapping over (a mapped type would mangle their methods into plain
|
|
13
|
+
* properties): functions, the toStringTag-less built-ins, and everything declaring `Symbol.toStringTag` (Map, Set,
|
|
14
|
+
* Promise, URL, typed arrays, streams, ...).
|
|
15
|
+
*/
|
|
16
|
+
type NonRecursive = AnyFunction | Date | RegExp | Error | {
|
|
17
|
+
readonly [Symbol.toStringTag]: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Makes a type deeply writable, keeping tuples, functions, and built-ins as-is. Symbol keys are kept: values that reach
|
|
21
|
+
* Clean land in the result as wholesale clones at runtime, which preserve them (and filtering them here would mangle
|
|
22
|
+
* interface-shaped values with symbol-keyed members, e.g. anything iterable). The `T extends object` gate comes first
|
|
23
|
+
* so primitive leaves (most nodes of a config type) cost one relation, and only recursion spends the depth budget.
|
|
24
|
+
*/
|
|
25
|
+
type Clean<T, Depth extends readonly unknown[] = MaxDepth> = T extends object ? Depth extends readonly [unknown, ...infer RemainingDepth] ? T extends NonRecursive ? T : { -readonly [K in keyof T]: Clean<T[K], RemainingDepth>; } : T : T;
|
|
26
|
+
/**
|
|
27
|
+
* In spread mode, base and user arrays concatenate to an element-union array. In replace mode, or over a non-array
|
|
28
|
+
* base, the user array is used as-is.
|
|
29
|
+
*/
|
|
30
|
+
type MergeArrays<BaseValue, UserValue extends readonly unknown[], Depth extends readonly unknown[], M extends Mode> = M extends "spread" ? BaseValue extends readonly unknown[] ? (Clean<BaseValue[number], Depth> | Clean<UserValue[number], Depth>)[] : Clean<UserValue, Depth> : Clean<UserValue, Depth>;
|
|
31
|
+
/**
|
|
32
|
+
* Merge one key's values: combine arrays per the mode, recurse into objects, else user wins. Gated on `UserValue
|
|
33
|
+
* extends object` first for the same primitive-leaf reason as Clean.
|
|
34
|
+
*/
|
|
35
|
+
type MergeValues<BaseValue, UserValue, Depth extends readonly unknown[], M extends Mode> = UserValue extends object ? UserValue extends readonly unknown[] ? MergeArrays<BaseValue, UserValue, Depth, M> : UserValue extends NonRecursive ? UserValue : BaseValue extends readonly unknown[] | NonRecursive ? Clean<UserValue, Depth> : BaseValue extends object ? MergeObjects<BaseValue, UserValue, Depth, M> : Clean<UserValue, Depth> : UserValue;
|
|
36
|
+
/**
|
|
37
|
+
* Merge two object types key by key, in base key order. An optional user key (only possible when the user passes a
|
|
38
|
+
* wide-typed value rather than a literal) may be absent at runtime, so it unions with the base value. User-only symbol
|
|
39
|
+
* keys are dropped because the runtime merge visits string keys only. The final `extends infer Merged` flattens the
|
|
40
|
+
* intersection through an anonymous mapped type: a named helper alias would show up as `SomeAlias<...>` in hovers
|
|
41
|
+
* instead of the plain object.
|
|
42
|
+
*/
|
|
43
|
+
type MergeObjects<BaseConfig, UserConfig, Depth extends readonly unknown[], M extends Mode> = Depth extends readonly [unknown, ...infer RemainingDepth] ? { -readonly [K in keyof BaseConfig]: K extends keyof UserConfig ? undefined extends UserConfig[K] ? MergeValues<BaseConfig[K], Exclude<UserConfig[K], undefined>, RemainingDepth, M> | Clean<BaseConfig[K], RemainingDepth> : MergeValues<BaseConfig[K], UserConfig[K], RemainingDepth, M> : Clean<BaseConfig[K], RemainingDepth>; } & { -readonly [K in keyof UserConfig as K extends keyof BaseConfig | symbol ? never : K]: Clean<UserConfig[K], RemainingDepth>; } extends (infer Merged) ? { [K in keyof Merged]: Merged[K]; } : never : UserConfig;
|
|
44
|
+
/**
|
|
45
|
+
* The merged result type, exact per key: spread-mode arrays show the element-union of base and user, replace-mode
|
|
46
|
+
* arrays show only the user's elements.
|
|
47
|
+
*/
|
|
48
|
+
type MergedConfig<BaseConfig, UserConfig, M extends Mode = "spread"> = MergeObjects<BaseConfig, UserConfig, MaxDepth, M>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { Clean, MergedConfig, Mode };
|
|
51
|
+
//# sourceMappingURL=merge.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.types.d.ts","names":[],"sources":["../../src/merge-config/merge.types.ts"],"mappings":";KAWK,kBAAkB;;KAGX;;;;;;KAOP;;;;;;KAOA,eAAe,cAAc,OAAO,SAAS;YAAoB,OAAO;;;;;;;;KAQjE,MAAM,GAAG,mCAAmC,YAAY,mBAChE,0CAA0C,kBACxC,UAAU,eACR,iBACa,WAAW,IAAI,MAAM,EAAE,IAAI,qBAC1C,IACF;;;;;KAMC,YACH,WACA,sCACA,kCACA,UAAU,QACR,qBACA,wCACG,MAAM,mBAAmB,SAAS,MAAM,mBAAmB,YAC5D,MAAM,WAAW,SACnB,MAAM,WAAW;;;;;KAMhB,YAAY,WAAW,WAAW,kCAAkC,UAAU,QAAQ,2BACvF,uCACE,YAAY,WAAW,WAAW,OAAO,KACzC,kBAAkB,eAChB,YACA,uCAAuC,eACrC,MAAM,WAAW,SACjB,2BACE,aAAa,WAAW,WAAW,OAAO,KAC1C,MAAM,WAAW,SACzB;;;;;;;;KASC,aAAa,YAAY,YAAY,kCAAkC,UAAU,QAAQ,0CAEnF,+BAGM,WAAW,aAAa,gBAAgB,+BAC7B,WAAW,KAEvB,YAAY,WAAW,IAAI,QAAQ,WAAW,gBAAgB,gBAAgB,KAC9E,MAAM,WAAW,IAAI,kBACzB,YAAY,WAAW,IAAI,WAAW,IAAI,gBAAgB,KAC5D,MAAM,WAAW,IAAI,kCAEd,WAAW,cAAc,gBAAgB,8BAA8B,IAAI,MACpF,WAAW,IACX,kCAEY,aACX,WAAW,SAAS,OAAO,gBAEhC;;;;;KAMQ,aAAa,YAAY,YAAY,UAAU,mBAAmB,aAC5E,YACA,YACA,UACA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adamhl8/configs",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "My shared configs for TS projects",
|
|
5
5
|
"homepage": "https://github.com/adamhl8/configs",
|
|
6
6
|
"bugs": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
"prepare": "just prepare"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adamhl8/eslint-plugin-clean-modules": "^0.1.
|
|
49
|
+
"@adamhl8/eslint-plugin-clean-modules": "^0.1.5",
|
|
50
50
|
"@commitlint/config-conventional": "^21.2.0",
|
|
51
51
|
"@commitlint/types": "^21.2.0",
|
|
52
52
|
"arkenv": "^0.12.2",
|
|
53
53
|
"arktype": "^2.2.3",
|
|
54
54
|
"es-toolkit": "^1.49.0",
|
|
55
55
|
"smol-toml": "^1.7.0",
|
|
56
|
-
"ts-explicit-errors": "^4.1.
|
|
56
|
+
"ts-explicit-errors": "^4.1.3",
|
|
57
57
|
"ts-extras": "^1.2.0",
|
|
58
58
|
"type-fest": "^5.8.0"
|
|
59
59
|
},
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"oxlint-tsgolint": "^0.24.0",
|
|
70
70
|
"publint": "^0.3.21",
|
|
71
71
|
"release-it": "^20.2.1",
|
|
72
|
-
"tsdown": "^0.22.
|
|
72
|
+
"tsdown": "^0.22.7",
|
|
73
73
|
"typescript": "^7.0.2"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
package/dist/utils.d.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { MergeDeep, ReadonlyDeep, SimplifyDeep, WritableDeep } from "type-fest";
|
|
2
|
-
//#region src/utils.d.ts
|
|
3
|
-
type AnyFunction = (...args: never[]) => unknown;
|
|
4
|
-
/**
|
|
5
|
-
* How a wrapped user array is combined with the base array. - `"merge"` (the default for plain arrays): the user array
|
|
6
|
-
* is appended to the base array - `"replace"`: the user array is used as-is
|
|
7
|
-
*/
|
|
8
|
-
type ArrayMergeMode = "merge" | "replace";
|
|
9
|
-
/**
|
|
10
|
-
* A per-key array override. Wherever a config key takes an array, `{ value: [...], mode: "merge" | "replace" }` may be
|
|
11
|
-
* passed instead; `mode` controls how that key's array combines with the base config's. The wrapper never appears in
|
|
12
|
-
* the merged result. A config value that is exactly this shape is always consumed as a wrapper; an object with extra
|
|
13
|
-
* keys is treated as a plain config object.
|
|
14
|
-
*/
|
|
15
|
-
interface ArrayOverride<Value extends readonly unknown[], Mode extends ArrayMergeMode = ArrayMergeMode> {
|
|
16
|
-
readonly value: Value;
|
|
17
|
-
readonly mode: Mode;
|
|
18
|
-
}
|
|
19
|
-
/** True only when T is exactly the wrapper shape (keys value + mode, nothing else), mirroring isArrayOverride. */
|
|
20
|
-
type IsArrayOverride<T> = T extends {
|
|
21
|
-
readonly value: readonly unknown[];
|
|
22
|
-
readonly mode: ArrayMergeMode;
|
|
23
|
-
} ? [keyof T] extends [keyof ArrayOverride<readonly unknown[]>] ? true : false : false;
|
|
24
|
-
/**
|
|
25
|
-
* For every array-typed value in T, additionally allow `{ value: <that array type>, mode: ArrayMergeMode }`.
|
|
26
|
-
* Distributes over unions, does not recurse into array elements, and leaves functions/primitives alone.
|
|
27
|
-
*/
|
|
28
|
-
type WithArrayOverrides<T> = T extends readonly unknown[] ? T | ArrayOverride<T> : T extends AnyFunction ? T : T extends object ? { [K in keyof T]: WithArrayOverrides<T[K]>; } : T;
|
|
29
|
-
/**
|
|
30
|
-
* True if T contains (at any object depth, not inside arrays) a replace-mode wrapper. Depth is a recursion budget: when
|
|
31
|
-
* a call fails the generic constraint, this type runs over the full constraint (the whole tool config type), and the
|
|
32
|
-
* eager `{ ... }[keyof T]` walk would hit TS2615 on circular types in it (e.g. Console) and bury the real error.
|
|
33
|
-
*/
|
|
34
|
-
type HasReplaceWrapper<T, Depth extends readonly unknown[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]> = Depth extends readonly [unknown, ...infer RemainingDepth] ? T extends readonly unknown[] ? false : T extends AnyFunction ? false : T extends object ? IsArrayOverride<T> extends true ? T extends {
|
|
35
|
-
readonly mode: "replace";
|
|
36
|
-
} ? true : false : true extends { [K in keyof T]: HasReplaceWrapper<T[K], RemainingDepth>; }[keyof T] ? true : false : false : false;
|
|
37
|
-
/** Keep only the key paths whose leaf is a replace-mode wrapper; unwrap those leaves to their `value` type. */
|
|
38
|
-
type DeepPickReplaceWrappers<T> = { [K in keyof T as HasReplaceWrapper<T[K]> extends true ? K : never]: IsArrayOverride<T[K]> extends true ? T[K] extends {
|
|
39
|
-
readonly value: infer V extends readonly unknown[];
|
|
40
|
-
} ? V : never : DeepPickReplaceWrappers<T[K]>; };
|
|
41
|
-
/** Drop key paths whose leaf is a replace-mode wrapper; unwrap merge-mode wrappers in place. */
|
|
42
|
-
type DeepDropReplaceWrappers<T> = { [K in keyof T as IsArrayOverride<T[K]> extends true ? T[K] extends {
|
|
43
|
-
readonly mode: "replace";
|
|
44
|
-
} ? never : K : K]: T[K] extends readonly unknown[] ? T[K] : T[K] extends AnyFunction ? T[K] : IsArrayOverride<T[K]> extends true ? T[K] extends {
|
|
45
|
-
readonly value: infer V extends readonly unknown[];
|
|
46
|
-
} ? V : never : T[K] extends object ? DeepDropReplaceWrappers<T[K]> : T[K]; };
|
|
47
|
-
/**
|
|
48
|
-
* The merged result type, exact for per-key overrides: pass 1 merges the user config minus replace-wrapper keys
|
|
49
|
-
* (unwrapping merge-mode wrappers, which behave like plain arrays) with array spreading; pass 2 merges the unwrapped
|
|
50
|
-
* replace-wrapper keys with array replacement. Instead of returning `BaseConfig & UserConfig` (from `merge`), this is a
|
|
51
|
-
* more friendly type via `MergeDeep`.
|
|
52
|
-
*/
|
|
53
|
-
type MergedConfig<BaseConfig, UserConfig> = MergeDeep<MergeDeep<SimplifyDeep<WritableDeep<BaseConfig>>, SimplifyDeep<WritableDeep<DeepDropReplaceWrappers<UserConfig>>>, {
|
|
54
|
-
arrayMergeMode: "spread";
|
|
55
|
-
}>, SimplifyDeep<WritableDeep<DeepPickReplaceWrappers<UserConfig>>>, {
|
|
56
|
-
arrayMergeMode: "replace";
|
|
57
|
-
}>;
|
|
58
|
-
/**
|
|
59
|
-
* The merge config function, where the `UserConfig` passed in is merged with `BaseConfig`.
|
|
60
|
-
*
|
|
61
|
-
* The `const` type parameter keeps literal types without requiring `as const` at the call site. The constraint is
|
|
62
|
-
* `WithArrayOverrides<ReadonlyDeep<...>>` so readonly arrays/objects (e.g. from `as const`) are accepted and any
|
|
63
|
-
* array-typed key can alternatively take an ArrayOverride wrapper; the result is `WritableDeep` so it stays assignable
|
|
64
|
-
* to the tools' own (mutable) config types.
|
|
65
|
-
*/
|
|
66
|
-
type MergeConfigFn<UserConfig, BaseConfig> = <const UserConfigToMerge extends WithArrayOverrides<ReadonlyDeep<UserConfig>>>(userConfig: UserConfigToMerge) => MergedConfig<BaseConfig, UserConfigToMerge>;
|
|
67
|
-
/**
|
|
68
|
-
* The optional merge config function, where the `userConfig` argument is optional. - if `UserConfig` is not provided,
|
|
69
|
-
* the return type is `BaseConfig` - if `UserConfig` is provided, the return type is the merged type of `BaseConfig` and
|
|
70
|
-
* `UserConfig`.
|
|
71
|
-
*
|
|
72
|
-
* CRITICAL: must stay a type-alias intersection (NOT `interface ... extends MergeConfigFn`): checking the
|
|
73
|
-
* interface-inheritance relation forces tsc to relate the no-arg signature's return type against the recursive
|
|
74
|
-
* `MergedConfig` type, which explodes to millions of type instantiations.
|
|
75
|
-
*/
|
|
76
|
-
type OptionalMergeConfigFn<UserConfig, BaseConfig> = MergeConfigFn<UserConfig, BaseConfig> & (() => SimplifyDeep<WritableDeep<BaseConfig>>);
|
|
77
|
-
//#endregion
|
|
78
|
-
export { MergeConfigFn, OptionalMergeConfigFn };
|
|
79
|
-
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","names":[],"sources":["../src/utils.ts"],"mappings":";;KAIK,kBAAkB;;;;;KAMlB;;;;;;;UAQK,cAAc,kCAAkC,aAAa,iBAAiB;WAC7E,OAAO;WACP,MAAM;;;KAiCZ,gBAAgB,KAAK;WAAqB;WAAoC,MAAM;WAC9E,kBAAkB;;;;;KASxB,mBAAmB,KAAK,+BACzB,IAAI,cAAc,KAClB,UAAU,cACR,IACA,sBACK,WAAW,IAAI,mBAAmB,EAAE,SACvC;;;;;;KAOH,kBAAkB,GAAG,qEAAqE,0CAEpF,kBAEP,uCAEE,UAAU,sBAER,mBACE,gBAAgB,kBACd;WAAqB;mCAGL,WAAW,IAAI,kBAAkB,EAAE,IAAI,yBAAwB;;KAOtF,wBAAwB,QAC1B,WAAW,KAAK,kBAAkB,EAAE,mBAAmB,YAAY,gBAAgB,EAAE,mBAClF,EAAE;WAAsB,aAAa;IACnC,YAEF,wBAAwB,EAAE;;KAI3B,wBAAwB,QAC1B,WAAW,KAAK,gBAAgB,EAAE,mBAC/B,EAAE;WAAsB;YAEtB,IACF,IAAI,EAAE,gCACN,EAAE,KACF,EAAE,WAAW,cACX,EAAE,KACF,gBAAgB,EAAE,mBAChB,EAAE;WAAsB,aAAa;IACnC,YAEF,EAAE,oBACA,wBAAwB,EAAE,MAC1B,EAAE;;;;;;;KAST,aAAa,YAAY,cAAc,UAC1C,UACE,aAAa,aAAa,cAC1B,aAAa,aAAa,wBAAwB;EAChD;IAEJ,aAAa,aAAa,wBAAwB;EAChD;;;;;;;;;;KAWQ,cAAc,YAAY,qBAC9B,0BAA0B,mBAAmB,aAAa,cAEhE,YAAY,sBACT,aAAa,YAAY;;;;;;;;;;KAWlB,sBAAsB,YAAY,cAAc,cAAc,YAAY,qBAC7E,aAAa,aAAa"}
|
package/dist/utils.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { cloneDeep, isPlainObject, mergeWith } from "es-toolkit";
|
|
2
|
-
//#region src/utils.ts
|
|
3
|
-
/** Runtime wrapper check, mirrored by IsArrayOverride: plain object with exactly the keys value (array) + mode. */
|
|
4
|
-
const isArrayOverride = (v) => {
|
|
5
|
-
if (!isPlainObject(v)) return false;
|
|
6
|
-
if (Object.keys(v).length !== 2) return false;
|
|
7
|
-
const { value } = v;
|
|
8
|
-
const { mode } = v;
|
|
9
|
-
return Array.isArray(value) && (mode === "merge" || mode === "replace");
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* A wrapper around es-toolkit's `mergeWith` with a custom merge function that concatenates arrays, honoring per-key
|
|
13
|
-
* ArrayOverride wrappers. Wrapper interception must come first: the default path would deep-merge the wrapper as a
|
|
14
|
-
* plain object.
|
|
15
|
-
*/
|
|
16
|
-
const merge = (target, source) => mergeWith(target, source, (objValue, srcValue) => {
|
|
17
|
-
if (isArrayOverride(srcValue)) {
|
|
18
|
-
const { value: userArray, mode } = srcValue;
|
|
19
|
-
if (!Array.isArray(objValue)) return userArray;
|
|
20
|
-
return mode === "merge" ? [...objValue, ...userArray] : userArray;
|
|
21
|
-
}
|
|
22
|
-
if (!Array.isArray(objValue)) return;
|
|
23
|
-
if (!Array.isArray(srcValue)) return srcValue;
|
|
24
|
-
const baseArray = objValue;
|
|
25
|
-
const userArray = srcValue;
|
|
26
|
-
return [...baseArray, ...userArray];
|
|
27
|
-
});
|
|
28
|
-
/**
|
|
29
|
-
* Creates a config merge function with proper types for merging with a base config.
|
|
30
|
-
*
|
|
31
|
-
* `UserConfig` is not inferrable, so both type arguments must be passed explicitly. Returning the exact
|
|
32
|
-
* `OptionalMergeConfigFn` instantiation (instead of relating a looser type to an annotation at the export site) keeps
|
|
33
|
-
* tsc from eagerly evaluating the deep-mapped types over the full config type, which blows the instantiation depth
|
|
34
|
-
* limit on large configs like tsdown's.
|
|
35
|
-
*/
|
|
36
|
-
const createMergeConfigFn = (baseConfig) => {
|
|
37
|
-
const mergeConfigFn = (userConfig) => {
|
|
38
|
-
if (userConfig === void 0) return cloneDeep(baseConfig);
|
|
39
|
-
return merge(cloneDeep(baseConfig), cloneDeep(userConfig));
|
|
40
|
-
};
|
|
41
|
-
return mergeConfigFn;
|
|
42
|
-
};
|
|
43
|
-
//#endregion
|
|
44
|
-
export { createMergeConfigFn };
|
|
45
|
-
|
|
46
|
-
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":["baseArray"],"sources":["../src/utils.ts"],"sourcesContent":["import { cloneDeep, isPlainObject, mergeWith } from \"es-toolkit\"\nimport type { MergeDeep, ReadonlyDeep, WritableDeep, SimplifyDeep } from \"type-fest\"\n\ntype AnyObj = object\ntype AnyFunction = (...args: never[]) => unknown\n\n/**\n * How a wrapped user array is combined with the base array. - `\"merge\"` (the default for plain arrays): the user array\n * is appended to the base array - `\"replace\"`: the user array is used as-is\n */\ntype ArrayMergeMode = \"merge\" | \"replace\"\n\n/**\n * A per-key array override. Wherever a config key takes an array, `{ value: [...], mode: \"merge\" | \"replace\" }` may be\n * passed instead; `mode` controls how that key's array combines with the base config's. The wrapper never appears in\n * the merged result. A config value that is exactly this shape is always consumed as a wrapper; an object with extra\n * keys is treated as a plain config object.\n */\ninterface ArrayOverride<Value extends readonly unknown[], Mode extends ArrayMergeMode = ArrayMergeMode> {\n readonly value: Value\n readonly mode: Mode\n}\n\n/** Runtime wrapper check, mirrored by IsArrayOverride: plain object with exactly the keys value (array) + mode. */\nconst isArrayOverride = (v: unknown): v is { value: unknown[]; mode: ArrayMergeMode } => {\n if (!isPlainObject(v)) return false\n if (Object.keys(v).length !== 2) return false\n const { value } = v\n const { mode } = v\n return Array.isArray(value) && (mode === \"merge\" || mode === \"replace\")\n}\n\n/**\n * A wrapper around es-toolkit's `mergeWith` with a custom merge function that concatenates arrays, honoring per-key\n * ArrayOverride wrappers. Wrapper interception must come first: the default path would deep-merge the wrapper as a\n * plain object.\n */\nconst merge = <T extends AnyObj, S extends AnyObj>(target: T, source: S): T & S =>\n mergeWith(target, source, (objValue: unknown, srcValue: unknown) => {\n if (isArrayOverride(srcValue)) {\n const { value: userArray, mode } = srcValue\n if (!Array.isArray(objValue)) return userArray\n const baseArray: unknown[] = objValue\n return mode === \"merge\" ? [...baseArray, ...userArray] : userArray\n }\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 [...baseArray, ...userArray]\n })\n\n/** True only when T is exactly the wrapper shape (keys value + mode, nothing else), mirroring isArrayOverride. */\ntype IsArrayOverride<T> = T extends { readonly value: readonly unknown[]; readonly mode: ArrayMergeMode }\n ? [keyof T] extends [keyof ArrayOverride<readonly unknown[]>]\n ? true\n : false\n : false\n\n/**\n * For every array-typed value in T, additionally allow `{ value: <that array type>, mode: ArrayMergeMode }`.\n * Distributes over unions, does not recurse into array elements, and leaves functions/primitives alone.\n */\ntype WithArrayOverrides<T> = T extends readonly unknown[]\n ? T | ArrayOverride<T>\n : T extends AnyFunction\n ? T\n : T extends object\n ? { [K in keyof T]: WithArrayOverrides<T[K]> }\n : T\n\n/**\n * True if T contains (at any object depth, not inside arrays) a replace-mode wrapper. Depth is a recursion budget: when\n * a call fails the generic constraint, this type runs over the full constraint (the whole tool config type), and the\n * eager `{ ... }[keyof T]` walk would hit TS2615 on circular types in it (e.g. Console) and bury the real error.\n */\ntype HasReplaceWrapper<T, Depth extends readonly unknown[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]> = Depth extends readonly [\n unknown,\n ...infer RemainingDepth,\n]\n ? T extends readonly unknown[]\n ? false\n : T extends AnyFunction\n ? false\n : T extends object\n ? IsArrayOverride<T> extends true\n ? T extends { readonly mode: \"replace\" }\n ? true\n : false\n : true extends { [K in keyof T]: HasReplaceWrapper<T[K], RemainingDepth> }[keyof T]\n ? true\n : false\n : false\n : false\n\n/** Keep only the key paths whose leaf is a replace-mode wrapper; unwrap those leaves to their `value` type. */\ntype DeepPickReplaceWrappers<T> = {\n [K in keyof T as HasReplaceWrapper<T[K]> extends true ? K : never]: IsArrayOverride<T[K]> extends true\n ? T[K] extends { readonly value: infer V extends readonly unknown[] }\n ? V\n : never\n : DeepPickReplaceWrappers<T[K]>\n}\n\n/** Drop key paths whose leaf is a replace-mode wrapper; unwrap merge-mode wrappers in place. */\ntype DeepDropReplaceWrappers<T> = {\n [K in keyof T as IsArrayOverride<T[K]> extends true\n ? T[K] extends { readonly mode: \"replace\" }\n ? never\n : K\n : K]: T[K] extends readonly unknown[]\n ? T[K]\n : T[K] extends AnyFunction\n ? T[K]\n : IsArrayOverride<T[K]> extends true\n ? T[K] extends { readonly value: infer V extends readonly unknown[] }\n ? V\n : never\n : T[K] extends object\n ? DeepDropReplaceWrappers<T[K]>\n : T[K]\n}\n\n/**\n * The merged result type, exact for per-key overrides: pass 1 merges the user config minus replace-wrapper keys\n * (unwrapping merge-mode wrappers, which behave like plain arrays) with array spreading; pass 2 merges the unwrapped\n * replace-wrapper keys with array replacement. Instead of returning `BaseConfig & UserConfig` (from `merge`), this is a\n * more friendly type via `MergeDeep`.\n */\ntype MergedConfig<BaseConfig, UserConfig> = MergeDeep<\n MergeDeep<\n SimplifyDeep<WritableDeep<BaseConfig>>,\n SimplifyDeep<WritableDeep<DeepDropReplaceWrappers<UserConfig>>>,\n { arrayMergeMode: \"spread\" }\n >,\n SimplifyDeep<WritableDeep<DeepPickReplaceWrappers<UserConfig>>>,\n { arrayMergeMode: \"replace\" }\n>\n\n/**\n * The merge config function, where the `UserConfig` passed in is merged with `BaseConfig`.\n *\n * The `const` type parameter keeps literal types without requiring `as const` at the call site. The constraint is\n * `WithArrayOverrides<ReadonlyDeep<...>>` so readonly arrays/objects (e.g. from `as const`) are accepted and any\n * array-typed key can alternatively take an ArrayOverride wrapper; the result is `WritableDeep` so it stays assignable\n * to the tools' own (mutable) config types.\n */\nexport type MergeConfigFn<UserConfig, BaseConfig> = <\n const UserConfigToMerge extends WithArrayOverrides<ReadonlyDeep<UserConfig>>,\n>(\n userConfig: UserConfigToMerge,\n) => MergedConfig<BaseConfig, UserConfigToMerge>\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 * CRITICAL: must stay a type-alias intersection (NOT `interface ... extends MergeConfigFn`): checking the\n * interface-inheritance relation forces tsc to relate the no-arg signature's return type against the recursive\n * `MergedConfig` type, which explodes to millions of type instantiations.\n */\nexport type OptionalMergeConfigFn<UserConfig, BaseConfig> = MergeConfigFn<UserConfig, BaseConfig> &\n (() => SimplifyDeep<WritableDeep<BaseConfig>>)\n\n/**\n * Creates a config merge function with proper types 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) => {\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))\n }\n\n // CRITICAL: two-step assertion required: a direct cast makes tsc run a comparability check relating the\n // implementation signature against the recursive MergedConfig type (millions of instantiations).\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion\n return mergeConfigFn as unknown as OptionalMergeConfigFn<UserConfig, BaseConfig>\n}\n"],"mappings":";;;AAwBA,MAAM,mBAAmB,MAAgE;CACvF,IAAI,CAAC,cAAc,CAAC,GAAG,OAAO;CAC9B,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,WAAW,GAAG,OAAO;CACxC,MAAM,EAAE,UAAU;CAClB,MAAM,EAAE,SAAS;CACjB,OAAO,MAAM,QAAQ,KAAK,MAAM,SAAS,WAAW,SAAS;AAC/D;;;;;;AAOA,MAAM,SAA6C,QAAW,WAC5D,UAAU,QAAQ,SAAS,UAAmB,aAAsB;CAClE,IAAI,gBAAgB,QAAQ,GAAG;EAC7B,MAAM,EAAE,OAAO,WAAW,SAAS;EACnC,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;EAErC,OAAO,SAAS,UAAU,CAAC,GAAGA,UAAW,GAAG,SAAS,IAAI;CAC3D;CACA,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;CAC9B,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACrC,MAAM,YAAuB;CAC7B,MAAM,YAAuB;CAC7B,OAAO,CAAC,GAAG,WAAW,GAAG,SAAS;AACpC,CAAC;;;;;;;;;AA2HH,MAAa,uBACX,eACkD;CAElD,MAAM,iBAAiB,eAAwB;EAG7C,IAAI,eAAe,KAAA,GAAW,OAAO,UAAU,UAAU;EAGzD,OAAO,MAAM,UAAU,UAAU,GAAG,UAAU,UAAU,CAAC;CAC3D;CAKA,OAAO;AACT"}
|