@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.
Files changed (60) hide show
  1. package/dist/PluginDriver-D0dY_hpJ.d.ts +1986 -0
  2. package/dist/{chunk-ByKO4r7w.cjs → chunk-MlS0t1Af.cjs} +15 -0
  3. package/dist/chunk-O_arW02_.js +17 -0
  4. package/dist/hooks.cjs +13 -28
  5. package/dist/hooks.cjs.map +1 -1
  6. package/dist/hooks.d.ts +11 -37
  7. package/dist/hooks.js +14 -28
  8. package/dist/hooks.js.map +1 -1
  9. package/dist/index.cjs +1469 -831
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +572 -191
  12. package/dist/index.js +1443 -826
  13. package/dist/index.js.map +1 -1
  14. package/package.json +7 -7
  15. package/src/Kubb.ts +38 -56
  16. package/src/KubbFile.ts +143 -0
  17. package/src/{PluginManager.ts → PluginDriver.ts} +159 -170
  18. package/src/build.ts +213 -65
  19. package/src/constants.ts +39 -6
  20. package/src/createAdapter.ts +25 -0
  21. package/src/createPlugin.ts +30 -0
  22. package/src/createStorage.ts +58 -0
  23. package/src/{config.ts → defineConfig.ts} +11 -16
  24. package/src/defineGenerator.ts +126 -0
  25. package/src/defineLogger.ts +13 -3
  26. package/src/defineParser.ts +57 -0
  27. package/src/definePresets.ts +16 -0
  28. package/src/defineResolver.ts +454 -0
  29. package/src/hooks/index.ts +1 -6
  30. package/src/hooks/useDriver.ts +11 -0
  31. package/src/hooks/useMode.ts +4 -4
  32. package/src/hooks/usePlugin.ts +3 -3
  33. package/src/index.ts +22 -10
  34. package/src/renderNode.tsx +25 -0
  35. package/src/storages/fsStorage.ts +2 -2
  36. package/src/storages/memoryStorage.ts +2 -2
  37. package/src/types.ts +639 -52
  38. package/src/utils/FunctionParams.ts +2 -2
  39. package/src/utils/TreeNode.ts +40 -2
  40. package/src/utils/diagnostics.ts +4 -1
  41. package/src/utils/executeStrategies.ts +29 -10
  42. package/src/utils/formatters.ts +10 -21
  43. package/src/utils/getBarrelFiles.ts +80 -10
  44. package/src/utils/getConfigs.ts +9 -23
  45. package/src/utils/getPreset.ts +78 -0
  46. package/src/utils/isInputPath.ts +8 -0
  47. package/src/utils/linters.ts +23 -3
  48. package/src/utils/packageJSON.ts +76 -0
  49. package/dist/chunk--u3MIqq1.js +0 -8
  50. package/dist/types-CiPWLv-5.d.ts +0 -1001
  51. package/src/BarrelManager.ts +0 -74
  52. package/src/PackageManager.ts +0 -180
  53. package/src/PromiseManager.ts +0 -40
  54. package/src/defineAdapter.ts +0 -22
  55. package/src/definePlugin.ts +0 -12
  56. package/src/defineStorage.ts +0 -56
  57. package/src/errors.ts +0 -1
  58. package/src/hooks/useKubb.ts +0 -22
  59. package/src/hooks/usePluginManager.ts +0 -11
  60. 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[]> = []
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path'
2
- import type { KubbFile } from '@kubb/fabric-core/types'
3
- import { getMode } from '../PluginManager.ts'
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)
@@ -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
- * Get diagnostic information for debugging
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
- * Chains promises
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
- * Chains promises, first non-null result stops and returns
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 an array of promise functions with optional concurrency limit.
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
- export type StrategySwitch<TStrategy extends Strategy, TInput extends Array<PromiseFunc<TValue, null>>, TValue> = TStrategy extends 'first'
76
- ? HookFirstOutput<TInput, TValue>
77
- : TStrategy extends 'seq'
78
- ? SeqOutput<TInput, TValue>
79
- : TStrategy extends 'parallel'
80
- ? HookParallelOutput<TInput, TValue>
81
- : never
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]
@@ -4,18 +4,13 @@ import type { formatters } from '../constants.ts'
4
4
  type Formatter = keyof typeof formatters
5
5
 
6
6
  /**
7
- * Check if a formatter command is available in the system.
7
+ * Returns `true` when the given formatter is installed and callable.
8
8
  *
9
- * @param formatter - The formatter to check ('biome', 'prettier', or 'oxfmt')
10
- * @returns Promise that resolves to true if the formatter is available, false otherwise
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
- * Detect which formatter is available in the system.
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
- * @remarks
32
- * Checks in order of preference: biome, oxfmt, prettier.
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
- * ```typescript
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 | undefined> {
47
- const formatterNames: Formatter[] = ['biome', 'oxfmt', 'prettier']
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 undefined
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 type { KubbFile } from '@kubb/fabric-core/types'
3
- import { BarrelManager } from '../BarrelManager.ts'
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
- export async function getBarrelFiles(files: Array<KubbFile.ResolvedFile>, { type, meta = {}, root, output }: AddIndexesProps): Promise<KubbFile.File[]> {
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 = barrelManager.getFiles({
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) => {
@@ -1,30 +1,16 @@
1
- import type { CLIOptions, ConfigInput } from '../config.ts'
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
- * Converting UserConfig to Config Array without a change in the object beside the JSON convert.
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 resolvedConfig: Promise<UserConfig | Array<UserConfig>> =
10
- typeof config === 'function' ? Promise.resolve(config(args as CLIOptions)) : Promise.resolve(config)
12
+ const resolved = await (typeof config === 'function' ? config(args as CLIOptions) : config)
13
+ const userConfigs = Array.isArray(resolved) ? resolved : [resolved]
11
14
 
12
- let userConfigs = await resolvedConfig
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
+ }
@@ -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
- export async function detectLinter(): Promise<Linter | undefined> {
16
- const linterNames: Linter[] = ['biome', 'oxlint', 'eslint']
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 undefined
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
+ }
@@ -1,8 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __defProp = Object.defineProperty;
3
- var __name = (target, value) => __defProp(target, "name", {
4
- value,
5
- configurable: true
6
- });
7
- //#endregion
8
- export { __name as t };