@kubb/core 5.0.0-alpha.3 → 5.0.0-alpha.31
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/dist/PluginDriver-D0dY_hpJ.d.ts +1986 -0
- package/dist/{chunk-ByKO4r7w.cjs → chunk-MlS0t1Af.cjs} +15 -0
- package/dist/chunk-O_arW02_.js +17 -0
- package/dist/hooks.cjs +13 -28
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +11 -37
- package/dist/hooks.js +14 -28
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +1469 -831
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +572 -191
- package/dist/index.js +1443 -826
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/Kubb.ts +38 -56
- package/src/KubbFile.ts +143 -0
- package/src/{PluginManager.ts → PluginDriver.ts} +159 -170
- package/src/build.ts +213 -65
- package/src/constants.ts +39 -6
- package/src/createAdapter.ts +25 -0
- package/src/createPlugin.ts +30 -0
- package/src/createStorage.ts +58 -0
- package/src/{config.ts → defineConfig.ts} +11 -16
- package/src/defineGenerator.ts +126 -0
- package/src/defineLogger.ts +13 -3
- package/src/defineParser.ts +57 -0
- package/src/definePresets.ts +16 -0
- package/src/defineResolver.ts +454 -0
- package/src/hooks/index.ts +1 -6
- package/src/hooks/useDriver.ts +11 -0
- package/src/hooks/useMode.ts +4 -4
- package/src/hooks/usePlugin.ts +3 -3
- package/src/index.ts +22 -10
- package/src/renderNode.tsx +25 -0
- package/src/storages/fsStorage.ts +2 -2
- package/src/storages/memoryStorage.ts +2 -2
- package/src/types.ts +639 -52
- package/src/utils/FunctionParams.ts +2 -2
- package/src/utils/TreeNode.ts +40 -2
- package/src/utils/diagnostics.ts +4 -1
- package/src/utils/executeStrategies.ts +29 -10
- package/src/utils/formatters.ts +10 -21
- package/src/utils/getBarrelFiles.ts +80 -10
- package/src/utils/getConfigs.ts +9 -23
- package/src/utils/getPreset.ts +78 -0
- package/src/utils/isInputPath.ts +8 -0
- package/src/utils/linters.ts +23 -3
- package/src/utils/packageJSON.ts +76 -0
- package/dist/chunk--u3MIqq1.js +0 -8
- package/dist/types-CiPWLv-5.d.ts +0 -1001
- package/src/BarrelManager.ts +0 -74
- package/src/PackageManager.ts +0 -180
- package/src/PromiseManager.ts +0 -40
- package/src/defineAdapter.ts +0 -22
- package/src/definePlugin.ts +0 -12
- package/src/defineStorage.ts +0 -56
- package/src/errors.ts +0 -1
- package/src/hooks/useKubb.ts +0 -22
- package/src/hooks/usePluginManager.ts +0 -11
- package/src/utils/getPlugins.ts +0 -23
|
@@ -30,12 +30,12 @@ type FunctionParamsASTWithType = {
|
|
|
30
30
|
default?: string
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* @deprecated
|
|
33
|
+
* @deprecated use ast package instead
|
|
34
34
|
*/
|
|
35
35
|
export type FunctionParamsAST = FunctionParamsASTWithoutType | FunctionParamsASTWithType
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* @deprecated
|
|
38
|
+
* @deprecated use ast package instead
|
|
39
39
|
*/
|
|
40
40
|
export class FunctionParams {
|
|
41
41
|
#items: Array<FunctionParamsAST | FunctionParamsAST[]> = []
|
package/src/utils/TreeNode.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import type
|
|
3
|
-
import { getMode } from '../
|
|
2
|
+
import type * as KubbFile from '../KubbFile.ts'
|
|
3
|
+
import { getMode } from '../PluginDriver.ts'
|
|
4
4
|
|
|
5
5
|
type BarrelData = {
|
|
6
6
|
file?: KubbFile.File
|
|
@@ -12,6 +12,15 @@ type BarrelData = {
|
|
|
12
12
|
name: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Tree structure used to build per-directory barrel (`index.ts`) files from a
|
|
17
|
+
* flat list of generated {@link KubbFile.File} entries.
|
|
18
|
+
*
|
|
19
|
+
* Each node represents either a directory or a file within the output tree.
|
|
20
|
+
* Use {@link TreeNode.build} to construct a root node from a file list, then
|
|
21
|
+
* traverse with {@link TreeNode.forEach}, {@link TreeNode.leaves}, or the
|
|
22
|
+
* `*Deep` helpers.
|
|
23
|
+
*/
|
|
15
24
|
export class TreeNode {
|
|
16
25
|
data: BarrelData
|
|
17
26
|
parent?: TreeNode
|
|
@@ -32,6 +41,9 @@ export class TreeNode {
|
|
|
32
41
|
return child
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns the root ancestor of this node, walking up via `parent` links.
|
|
46
|
+
*/
|
|
35
47
|
get root(): TreeNode {
|
|
36
48
|
if (!this.parent) {
|
|
37
49
|
return this
|
|
@@ -39,6 +51,11 @@ export class TreeNode {
|
|
|
39
51
|
return this.parent.root
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Returns all leaf descendants (nodes with no children) of this node.
|
|
56
|
+
*
|
|
57
|
+
* Results are cached after the first traversal.
|
|
58
|
+
*/
|
|
42
59
|
get leaves(): Array<TreeNode> {
|
|
43
60
|
if (!this.children || this.children.length === 0) {
|
|
44
61
|
// this is a leaf
|
|
@@ -59,6 +76,9 @@ export class TreeNode {
|
|
|
59
76
|
return leaves
|
|
60
77
|
}
|
|
61
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Visits this node and every descendant in depth-first order.
|
|
81
|
+
*/
|
|
62
82
|
forEach(callback: (treeNode: TreeNode) => void): this {
|
|
63
83
|
if (typeof callback !== 'function') {
|
|
64
84
|
throw new TypeError('forEach() callback must be a function')
|
|
@@ -73,6 +93,9 @@ export class TreeNode {
|
|
|
73
93
|
return this
|
|
74
94
|
}
|
|
75
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Finds the first leaf that satisfies `predicate`, or `undefined` when none match.
|
|
98
|
+
*/
|
|
76
99
|
findDeep(predicate?: (value: TreeNode, index: number, obj: TreeNode[]) => boolean): TreeNode | undefined {
|
|
77
100
|
if (typeof predicate !== 'function') {
|
|
78
101
|
throw new TypeError('find() predicate must be a function')
|
|
@@ -81,6 +104,9 @@ export class TreeNode {
|
|
|
81
104
|
return this.leaves.find(predicate)
|
|
82
105
|
}
|
|
83
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Calls `callback` for every leaf of this node.
|
|
109
|
+
*/
|
|
84
110
|
forEachDeep(callback: (treeNode: TreeNode) => void): void {
|
|
85
111
|
if (typeof callback !== 'function') {
|
|
86
112
|
throw new TypeError('forEach() callback must be a function')
|
|
@@ -89,6 +115,9 @@ export class TreeNode {
|
|
|
89
115
|
this.leaves.forEach(callback)
|
|
90
116
|
}
|
|
91
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Returns all leaves that satisfy `callback`.
|
|
120
|
+
*/
|
|
92
121
|
filterDeep(callback: (treeNode: TreeNode) => boolean): Array<TreeNode> {
|
|
93
122
|
if (typeof callback !== 'function') {
|
|
94
123
|
throw new TypeError('filter() callback must be a function')
|
|
@@ -97,6 +126,9 @@ export class TreeNode {
|
|
|
97
126
|
return this.leaves.filter(callback)
|
|
98
127
|
}
|
|
99
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Maps every leaf through `callback` and returns the resulting array.
|
|
131
|
+
*/
|
|
100
132
|
mapDeep<T>(callback: (treeNode: TreeNode) => T): Array<T> {
|
|
101
133
|
if (typeof callback !== 'function') {
|
|
102
134
|
throw new TypeError('map() callback must be a function')
|
|
@@ -105,6 +137,12 @@ export class TreeNode {
|
|
|
105
137
|
return this.leaves.map(callback)
|
|
106
138
|
}
|
|
107
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Builds a {@link TreeNode} tree from a flat list of files.
|
|
142
|
+
*
|
|
143
|
+
* - Filters to files under `root` (when provided) and skips `.json` files.
|
|
144
|
+
* - Returns `null` when no files match.
|
|
145
|
+
*/
|
|
108
146
|
public static build(files: KubbFile.File[], root?: string): TreeNode | null {
|
|
109
147
|
try {
|
|
110
148
|
const filteredTree = buildDirectoryTree(files, root)
|
package/src/utils/diagnostics.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { version as nodeVersion } from 'node:process'
|
|
|
2
2
|
import { version as KubbVersion } from '../../package.json'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Returns a snapshot of the current runtime environment.
|
|
6
|
+
*
|
|
7
|
+
* Useful for attaching context to debug logs and error reports so that
|
|
8
|
+
* issues can be reproduced without manual information gathering.
|
|
6
9
|
*/
|
|
7
10
|
export function getDiagnosticInfo() {
|
|
8
11
|
return {
|
|
@@ -7,7 +7,12 @@ type ValueOfPromiseFuncArray<TInput extends Array<unknown>> = TInput extends Arr
|
|
|
7
7
|
type SeqOutput<TInput extends Array<PromiseFunc<TValue, null>>, TValue> = Promise<Array<Awaited<ValueOfPromiseFuncArray<TInput>>>>
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Runs promise functions in sequence, threading each result into the next call.
|
|
11
|
+
*
|
|
12
|
+
* - Each function receives the accumulated state from the previous call.
|
|
13
|
+
* - Skips functions that return a falsy value (acts as a no-op for that step).
|
|
14
|
+
* - Returns an array of all individual results.
|
|
15
|
+
* @deprecated
|
|
11
16
|
*/
|
|
12
17
|
export function hookSeq<TInput extends Array<PromiseFunc<TValue, null>>, TValue, TOutput = SeqOutput<TInput, TValue>>(promises: TInput): TOutput {
|
|
13
18
|
return promises.filter(Boolean).reduce(
|
|
@@ -33,7 +38,11 @@ export function hookSeq<TInput extends Array<PromiseFunc<TValue, null>>, TValue,
|
|
|
33
38
|
type HookFirstOutput<TInput extends Array<PromiseFunc<TValue, null>>, TValue = unknown> = ValueOfPromiseFuncArray<TInput>
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
*
|
|
41
|
+
* Runs promise functions in sequence and returns the first non-null result.
|
|
42
|
+
*
|
|
43
|
+
* - Stops as soon as `nullCheck` passes for a result (default: `!== null`).
|
|
44
|
+
* - Subsequent functions are skipped once a match is found.
|
|
45
|
+
* @deprecated
|
|
37
46
|
*/
|
|
38
47
|
export function hookFirst<TInput extends Array<PromiseFunc<TValue, null>>, TValue = unknown, TOutput = HookFirstOutput<TInput, TValue>>(
|
|
39
48
|
promises: TInput,
|
|
@@ -57,7 +66,11 @@ export function hookFirst<TInput extends Array<PromiseFunc<TValue, null>>, TValu
|
|
|
57
66
|
type HookParallelOutput<TInput extends Array<PromiseFunc<TValue, null>>, TValue> = Promise<PromiseSettledResult<Awaited<ValueOfPromiseFuncArray<TInput>>>[]>
|
|
58
67
|
|
|
59
68
|
/**
|
|
60
|
-
* Runs
|
|
69
|
+
* Runs promise functions concurrently and returns all settled results.
|
|
70
|
+
*
|
|
71
|
+
* - Limits simultaneous executions to `concurrency` (default: unlimited).
|
|
72
|
+
* - Uses `Promise.allSettled` so individual failures do not cancel other tasks.
|
|
73
|
+
* @deprecated
|
|
61
74
|
*/
|
|
62
75
|
export function hookParallel<TInput extends Array<PromiseFunc<TValue, null>>, TValue = unknown, TOutput = HookParallelOutput<TInput, TValue>>(
|
|
63
76
|
promises: TInput,
|
|
@@ -70,12 +83,18 @@ export function hookParallel<TInput extends Array<PromiseFunc<TValue, null>>, TV
|
|
|
70
83
|
return Promise.allSettled(tasks) as TOutput
|
|
71
84
|
}
|
|
72
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Execution strategy used when dispatching plugin hook calls.
|
|
88
|
+
* @deprecated
|
|
89
|
+
*/
|
|
73
90
|
export type Strategy = 'seq' | 'first' | 'parallel'
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
type StrategyOutputMap<TInput extends Array<PromiseFunc<TValue, null>>, TValue> = {
|
|
93
|
+
first: HookFirstOutput<TInput, TValue>
|
|
94
|
+
seq: SeqOutput<TInput, TValue>
|
|
95
|
+
parallel: HookParallelOutput<TInput, TValue>
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated
|
|
99
|
+
*/
|
|
100
|
+
export type StrategySwitch<TStrategy extends Strategy, TInput extends Array<PromiseFunc<TValue, null>>, TValue> = StrategyOutputMap<TInput, TValue>[TStrategy]
|
package/src/utils/formatters.ts
CHANGED
|
@@ -4,18 +4,13 @@ import type { formatters } from '../constants.ts'
|
|
|
4
4
|
type Formatter = keyof typeof formatters
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Returns `true` when the given formatter is installed and callable.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @remarks
|
|
13
|
-
* This function checks availability by running `<formatter> --version` command.
|
|
14
|
-
* All supported formatters (biome, prettier, oxfmt) implement the --version flag.
|
|
9
|
+
* Availability is detected by running `<formatter> --version` and checking
|
|
10
|
+
* that the process exits without error.
|
|
15
11
|
*/
|
|
16
12
|
async function isFormatterAvailable(formatter: Formatter): Promise<boolean> {
|
|
17
13
|
try {
|
|
18
|
-
// Try to get the version of the formatter to check if it's installed
|
|
19
14
|
await x(formatter, ['--version'], { nodeOptions: { stdio: 'ignore' } })
|
|
20
15
|
return true
|
|
21
16
|
} catch {
|
|
@@ -24,27 +19,21 @@ async function isFormatterAvailable(formatter: Formatter): Promise<boolean> {
|
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @returns Promise that resolves to the first available formatter or undefined if none are found
|
|
22
|
+
* Detects the first available code formatter on the current system.
|
|
30
23
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Uses the `--version` flag to detect if each formatter command is available.
|
|
34
|
-
* This is a reliable method as all supported formatters implement this flag.
|
|
24
|
+
* - Checks in preference order: `biome`, `oxfmt`, `prettier`.
|
|
25
|
+
* - Returns `null` when none are found.
|
|
35
26
|
*
|
|
36
27
|
* @example
|
|
37
|
-
* ```
|
|
28
|
+
* ```ts
|
|
38
29
|
* const formatter = await detectFormatter()
|
|
39
30
|
* if (formatter) {
|
|
40
31
|
* console.log(`Using ${formatter} for formatting`)
|
|
41
|
-
* } else {
|
|
42
|
-
* console.log('No formatter found')
|
|
43
32
|
* }
|
|
44
33
|
* ```
|
|
45
34
|
*/
|
|
46
|
-
export async function detectFormatter(): Promise<Formatter |
|
|
47
|
-
const formatterNames
|
|
35
|
+
export async function detectFormatter(): Promise<Formatter | null> {
|
|
36
|
+
const formatterNames = new Set(['biome', 'oxfmt', 'prettier'] as const)
|
|
48
37
|
|
|
49
38
|
for (const formatter of formatterNames) {
|
|
50
39
|
if (await isFormatterAvailable(formatter)) {
|
|
@@ -52,5 +41,5 @@ export async function detectFormatter(): Promise<Formatter | undefined> {
|
|
|
52
41
|
}
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
return
|
|
44
|
+
return null
|
|
56
45
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/useIterableCallbackReturn: not needed */
|
|
1
2
|
import { join } from 'node:path'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
3
|
+
import { getRelativePath } from '@internals/utils'
|
|
4
|
+
import type * as KubbFile from '../KubbFile.ts'
|
|
4
5
|
import type { BarrelType } from '../types.ts'
|
|
6
|
+
import { TreeNode } from './TreeNode.ts'
|
|
5
7
|
|
|
6
8
|
export type FileMetaBase = {
|
|
7
9
|
pluginName?: string
|
|
@@ -27,6 +29,72 @@ type AddIndexesProps = {
|
|
|
27
29
|
meta?: FileMetaBase
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function getBarrelFilesByRoot(root: string | undefined, files: Array<KubbFile.ResolvedFile>): Array<KubbFile.File> {
|
|
33
|
+
const cachedFiles = new Map<KubbFile.Path, KubbFile.File>()
|
|
34
|
+
|
|
35
|
+
TreeNode.build(files, root)?.forEach((treeNode) => {
|
|
36
|
+
if (!treeNode?.children || !treeNode.parent?.data.path) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const barrelFilePath = join(treeNode.parent?.data.path, 'index.ts') as KubbFile.Path
|
|
41
|
+
const barrelFile: KubbFile.File = {
|
|
42
|
+
path: barrelFilePath,
|
|
43
|
+
baseName: 'index.ts',
|
|
44
|
+
exports: [],
|
|
45
|
+
imports: [],
|
|
46
|
+
sources: [],
|
|
47
|
+
}
|
|
48
|
+
const previousBarrelFile = cachedFiles.get(barrelFile.path)
|
|
49
|
+
const leaves = treeNode.leaves
|
|
50
|
+
|
|
51
|
+
leaves.forEach((item) => {
|
|
52
|
+
if (!item.data.name) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sources = item.data.file?.sources || []
|
|
57
|
+
|
|
58
|
+
sources.forEach((source) => {
|
|
59
|
+
if (!item.data.file?.path || !source.isIndexable || !source.name) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
const alreadyContainInPreviousBarrelFile = previousBarrelFile?.sources.some(
|
|
63
|
+
(item) => item.name === source.name && item.isTypeOnly === source.isTypeOnly,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (alreadyContainInPreviousBarrelFile) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
barrelFile.exports!.push({
|
|
71
|
+
name: [source.name],
|
|
72
|
+
path: getRelativePath(treeNode.parent?.data.path, item.data.path),
|
|
73
|
+
isTypeOnly: source.isTypeOnly,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
barrelFile.sources.push({
|
|
77
|
+
name: source.name,
|
|
78
|
+
isTypeOnly: source.isTypeOnly,
|
|
79
|
+
//TODO use parser to generate import
|
|
80
|
+
value: '',
|
|
81
|
+
isExportable: false,
|
|
82
|
+
isIndexable: false,
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (previousBarrelFile) {
|
|
88
|
+
previousBarrelFile.sources.push(...barrelFile.sources)
|
|
89
|
+
previousBarrelFile.exports?.push(...(barrelFile.exports || []))
|
|
90
|
+
} else {
|
|
91
|
+
cachedFiles.set(barrelFile.path, barrelFile)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return [...cachedFiles.values()]
|
|
96
|
+
}
|
|
97
|
+
|
|
30
98
|
function trimExtName(text: string): string {
|
|
31
99
|
const dotIndex = text.lastIndexOf('.')
|
|
32
100
|
// Only strip when the dot is found and no path separator follows it
|
|
@@ -37,24 +105,26 @@ function trimExtName(text: string): string {
|
|
|
37
105
|
return text
|
|
38
106
|
}
|
|
39
107
|
|
|
40
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Generates `index.ts` barrel files for all directories under `root/output.path`.
|
|
110
|
+
*
|
|
111
|
+
* - Returns an empty array when `type` is falsy or `'propagate'`.
|
|
112
|
+
* - Skips generation when the output path itself ends with `index` (already a barrel).
|
|
113
|
+
* - When `type` is `'all'`, strips named exports so every re-export becomes a wildcard (`export * from`).
|
|
114
|
+
* - Attaches `meta` to each barrel file for downstream plugin identification.
|
|
115
|
+
*/
|
|
116
|
+
export async function getBarrelFiles(files: Array<KubbFile.ResolvedFile>, { type, meta = {}, root, output }: AddIndexesProps): Promise<Array<KubbFile.File>> {
|
|
41
117
|
if (!type || type === 'propagate') {
|
|
42
118
|
return []
|
|
43
119
|
}
|
|
44
120
|
|
|
45
|
-
const barrelManager = new BarrelManager()
|
|
46
|
-
|
|
47
121
|
const pathToBuildFrom = join(root, output.path)
|
|
48
122
|
|
|
49
123
|
if (trimExtName(pathToBuildFrom).endsWith('index')) {
|
|
50
124
|
return []
|
|
51
125
|
}
|
|
52
126
|
|
|
53
|
-
const barrelFiles =
|
|
54
|
-
files,
|
|
55
|
-
root: pathToBuildFrom,
|
|
56
|
-
meta,
|
|
57
|
-
})
|
|
127
|
+
const barrelFiles = getBarrelFilesByRoot(pathToBuildFrom, files)
|
|
58
128
|
|
|
59
129
|
if (type === 'all') {
|
|
60
130
|
return barrelFiles.map((file) => {
|
package/src/utils/getConfigs.ts
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
import type { CLIOptions, ConfigInput } from '../
|
|
1
|
+
import type { CLIOptions, ConfigInput } from '../defineConfig.ts'
|
|
2
2
|
import type { Config, UserConfig } from '../types.ts'
|
|
3
|
-
import { getPlugins } from './getPlugins.ts'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Resolves a {@link ConfigInput} into a normalized array of {@link Config} objects.
|
|
6
|
+
*
|
|
7
|
+
* - Awaits the config when it is a `Promise`.
|
|
8
|
+
* - Calls the factory function with `args` when the config is a function.
|
|
9
|
+
* - Wraps a single config object in an array for uniform downstream handling.
|
|
7
10
|
*/
|
|
8
11
|
export async function getConfigs(config: ConfigInput | UserConfig, args: CLIOptions): Promise<Array<Config>> {
|
|
9
|
-
const
|
|
10
|
-
|
|
12
|
+
const resolved = await (typeof config === 'function' ? config(args as CLIOptions) : config)
|
|
13
|
+
const userConfigs = Array.isArray(resolved) ? resolved : [resolved]
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!Array.isArray(userConfigs)) {
|
|
15
|
-
userConfigs = [userConfigs]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const results: Array<Config> = []
|
|
19
|
-
|
|
20
|
-
for (const item of userConfigs) {
|
|
21
|
-
const plugins = item.plugins ? await getPlugins(item.plugins) : undefined
|
|
22
|
-
|
|
23
|
-
results.push({
|
|
24
|
-
...item,
|
|
25
|
-
plugins,
|
|
26
|
-
} as Config)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return results
|
|
15
|
+
return userConfigs.map((item) => ({ plugins: [], ...item }) as Config)
|
|
30
16
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { composeTransformers } from '@kubb/ast'
|
|
2
|
+
import type { Visitor } from '@kubb/ast/types'
|
|
3
|
+
import type { CompatibilityPreset, Generator, Preset, Presets, Resolver } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a copy of `defaults` where each function in `userOverrides` is wrapped
|
|
7
|
+
* so a `null`/`undefined` return falls back to the original. Non-function values
|
|
8
|
+
* are assigned directly. All calls use the merged object as `this`.
|
|
9
|
+
*/
|
|
10
|
+
function withFallback<T extends object>(defaults: T, userOverrides: Partial<T>): T {
|
|
11
|
+
const merged = { ...defaults } as T
|
|
12
|
+
|
|
13
|
+
for (const key of Object.keys(userOverrides) as Array<keyof T>) {
|
|
14
|
+
const userVal = userOverrides[key]
|
|
15
|
+
const defaultVal = defaults[key]
|
|
16
|
+
|
|
17
|
+
if (typeof userVal === 'function' && typeof defaultVal === 'function') {
|
|
18
|
+
;(merged as any)[key] = (...args: any[]) => (userVal as Function).apply(merged, args) ?? (defaultVal as Function).apply(merged, args)
|
|
19
|
+
} else if (userVal !== undefined) {
|
|
20
|
+
merged[key] = userVal as T[typeof key]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return merged
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type GetPresetParams<TResolver extends Resolver> = {
|
|
28
|
+
preset: CompatibilityPreset
|
|
29
|
+
presets: Presets<TResolver>
|
|
30
|
+
/**
|
|
31
|
+
* Optional single resolver whose methods override the preset resolver.
|
|
32
|
+
* When a method returns `null` or `undefined` the preset resolver's method is used instead.
|
|
33
|
+
*/
|
|
34
|
+
resolver?: Partial<TResolver> & ThisType<TResolver>
|
|
35
|
+
/**
|
|
36
|
+
* User-supplied generators to append after the preset's generators.
|
|
37
|
+
*/
|
|
38
|
+
generators?: Array<Generator<any>>
|
|
39
|
+
/**
|
|
40
|
+
* Optional single transformer visitor whose methods override the preset transformer.
|
|
41
|
+
* When a method returns `null` or `undefined` the preset transformer's method is used instead.
|
|
42
|
+
*/
|
|
43
|
+
transformer?: Visitor
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type GetPresetResult<TResolver extends Resolver> = {
|
|
47
|
+
resolver: TResolver
|
|
48
|
+
transformer: Visitor | undefined
|
|
49
|
+
generators: Array<Generator<any>>
|
|
50
|
+
preset: Preset<TResolver> | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves a named preset into a resolver, transformer, and generators.
|
|
55
|
+
*
|
|
56
|
+
* - Selects the preset resolver; wraps it with user overrides using null/undefined fallback.
|
|
57
|
+
* - Composes the preset's transformers into a single visitor; wraps it with the user transformer using null/undefined fallback.
|
|
58
|
+
* - Combines preset generators with user-supplied generators; falls back to the `default` preset's generators when neither provides any.
|
|
59
|
+
*/
|
|
60
|
+
export function getPreset<TResolver extends Resolver = Resolver>(params: GetPresetParams<TResolver>): GetPresetResult<TResolver> {
|
|
61
|
+
const { preset: presetName, presets, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = params
|
|
62
|
+
const preset = presets[presetName]
|
|
63
|
+
|
|
64
|
+
const presetResolver = preset?.resolver ?? presets['default']!.resolver
|
|
65
|
+
const resolver = userResolver ? withFallback(presetResolver, userResolver) : presetResolver
|
|
66
|
+
|
|
67
|
+
const presetTransformers = preset?.transformers ?? []
|
|
68
|
+
const presetTransformer = presetTransformers.length > 0 ? composeTransformers(...presetTransformers) : undefined
|
|
69
|
+
const transformer = presetTransformer && userTransformer ? withFallback(presetTransformer, userTransformer) : (userTransformer ?? presetTransformer)
|
|
70
|
+
|
|
71
|
+
const presetGenerators = preset?.generators ?? []
|
|
72
|
+
const defaultGenerators = presets['default']?.generators ?? []
|
|
73
|
+
const generators = (presetGenerators.length > 0 || userGenerators.length > 0 ? [...presetGenerators, ...userGenerators] : defaultGenerators) as Array<
|
|
74
|
+
Generator<any>
|
|
75
|
+
>
|
|
76
|
+
|
|
77
|
+
return { resolver, transformer, generators, preset }
|
|
78
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InputPath, UserConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard to check if a given config has an `input.path`.
|
|
5
|
+
*/
|
|
6
|
+
export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> {
|
|
7
|
+
return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
|
|
8
|
+
}
|
package/src/utils/linters.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { linters } from '../constants.ts'
|
|
|
3
3
|
|
|
4
4
|
type Linter = keyof typeof linters
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Returns `true` when the given linter is installed and callable.
|
|
8
|
+
*
|
|
9
|
+
* Availability is detected by running `<linter> --version` and checking
|
|
10
|
+
* that the process exits without error.
|
|
11
|
+
*/
|
|
6
12
|
async function isLinterAvailable(linter: Linter): Promise<boolean> {
|
|
7
13
|
try {
|
|
8
14
|
await x(linter, ['--version'], { nodeOptions: { stdio: 'ignore' } })
|
|
@@ -12,8 +18,22 @@ async function isLinterAvailable(linter: Linter): Promise<boolean> {
|
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Detects the first available linter on the current system.
|
|
23
|
+
*
|
|
24
|
+
* - Checks in preference order: `biome`, `oxlint`, `eslint`.
|
|
25
|
+
* - Returns `null` when none are found.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const linter = await detectLinter()
|
|
30
|
+
* if (linter) {
|
|
31
|
+
* console.log(`Using ${linter} for linting`)
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export async function detectLinter(): Promise<Linter | null> {
|
|
36
|
+
const linterNames = new Set(['biome', 'oxlint', 'eslint'] as const)
|
|
17
37
|
|
|
18
38
|
for (const linter of linterNames) {
|
|
19
39
|
if (await isLinterAvailable(linter)) {
|
|
@@ -21,5 +41,5 @@ export async function detectLinter(): Promise<Linter | undefined> {
|
|
|
21
41
|
}
|
|
22
42
|
}
|
|
23
43
|
|
|
24
|
-
return
|
|
44
|
+
return null
|
|
25
45
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readSync } from '@internals/utils'
|
|
2
|
+
import * as pkg from 'empathic/package'
|
|
3
|
+
import { coerce, satisfies } from 'semver'
|
|
4
|
+
|
|
5
|
+
type PackageJSON = {
|
|
6
|
+
dependencies?: Record<string, string>
|
|
7
|
+
devDependencies?: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type DependencyName = string
|
|
11
|
+
type DependencyVersion = string
|
|
12
|
+
|
|
13
|
+
function getPackageJSONSync(cwd?: string): PackageJSON | null {
|
|
14
|
+
const pkgPath = pkg.up({ cwd })
|
|
15
|
+
if (!pkgPath) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return JSON.parse(readSync(pkgPath)) as PackageJSON
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function match(packageJSON: PackageJSON, dependency: DependencyName | RegExp): string | null {
|
|
23
|
+
const dependencies = {
|
|
24
|
+
...(packageJSON.dependencies || {}),
|
|
25
|
+
...(packageJSON.devDependencies || {}),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof dependency === 'string' && dependencies[dependency]) {
|
|
29
|
+
return dependencies[dependency]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const matched = Object.keys(dependencies).find((dep) => dep.match(dependency))
|
|
33
|
+
|
|
34
|
+
return matched ? (dependencies[matched] ?? null) : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getVersionSync(dependency: DependencyName | RegExp, cwd?: string): DependencyVersion | null {
|
|
38
|
+
const packageJSON = getPackageJSONSync(cwd)
|
|
39
|
+
|
|
40
|
+
return packageJSON ? match(packageJSON, dependency) : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns `true` when the nearest `package.json` declares a dependency that
|
|
45
|
+
* satisfies the given semver range.
|
|
46
|
+
*
|
|
47
|
+
* - Searches both `dependencies` and `devDependencies`.
|
|
48
|
+
* - Accepts a string package name or a `RegExp` to match scoped/pattern packages.
|
|
49
|
+
* - Uses `semver.satisfies` for range comparison; returns `false` when the
|
|
50
|
+
* version string cannot be coerced into a valid semver.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* satisfiesDependency('react', '>=18') // true when react@18.x is installed
|
|
55
|
+
* satisfiesDependency(/^@tanstack\//, '>=5') // true when any @tanstack/* >=5 is found
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function satisfiesDependency(dependency: DependencyName | RegExp, version: DependencyVersion, cwd?: string): boolean {
|
|
59
|
+
const packageVersion = getVersionSync(dependency, cwd)
|
|
60
|
+
|
|
61
|
+
if (!packageVersion) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (packageVersion === version) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const semVer = coerce(packageVersion)
|
|
70
|
+
|
|
71
|
+
if (!semVer) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return satisfies(semVer, version)
|
|
76
|
+
}
|