@kubb/middleware-barrel 5.0.0-alpha.62 → 5.0.0-alpha.64

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.
@@ -1,208 +1,237 @@
1
+ import { extname } from 'node:path'
1
2
  import { createExport, createFile } from '@kubb/ast'
2
- import type { FileNode } from '@kubb/ast'
3
+ import type { ExportNode, FileNode, SourceNode } from '@kubb/ast'
3
4
  import { BARREL_FILENAME } from '../constants.ts'
4
5
  import type { BarrelType } from '../types.ts'
5
- import { buildTree, type BuildTree } from './buildTree.ts'
6
+ import { type BuildTree, buildTree } from '@internals/utils'
6
7
 
7
8
  const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
9
+ const BARREL_SUFFIX = `/${BARREL_FILENAME}`
8
10
 
9
11
  /**
10
- * Derives a relative module specifier from an absolute `filePath` relative to an absolute `fromDir`.
11
- * The source extension is preserved so that `@kubb/parser-ts` can apply the `extNames` mapping
12
- * (e.g. `.ts` → `.js` for ESM output).
12
+ * Derives a relative module specifier from `filePath` relative to `fromDir`.
13
+ * The source extension is preserved so `@kubb/parser-ts` can apply its `extNames` mapping.
13
14
  *
14
15
  * @example
16
+ * ```ts
15
17
  * toRelativeModulePath('/src/gen/types', '/src/gen/types/pet.ts') // './pet.ts'
16
18
  * toRelativeModulePath('/src/gen/types', '/src/gen/types/tags/tag.ts') // './tags/tag.ts'
19
+ * ```
17
20
  */
18
21
  function toRelativeModulePath(fromDir: string, filePath: string): string {
19
- const relative = filePath.slice(fromDir.length).replace(/^[/\\]/g, '')
20
- return `./${relative}`
22
+ return `./${filePath.slice(fromDir.length + 1)}`
21
23
  }
22
24
 
23
- /**
24
- * Generates barrel `FileNode[]` for a given directory tree node using the `'all'` strategy:
25
- * each leaf file gets a `export * from './relPath'` in the barrel of its nearest ancestor directory.
26
- *
27
- * Only a single barrel file (at `treeNode.path`) is generated — sub-directory files are referenced
28
- * with their full relative path from `treeNode.path`.
29
- */
30
- function getBarrelFilesAll(treeNode: BuildTree, sourceFiles: ReadonlyArray<FileNode>): Array<FileNode> {
31
- // Collect all source file paths under this node (excluding barrel files themselves)
32
- const leafPaths = collectLeafPaths(treeNode).filter((p) => !p.endsWith(`/${BARREL_FILENAME}`))
25
+ function isBarrelPath(path: string): boolean {
26
+ return path.endsWith(BARREL_SUFFIX)
27
+ }
33
28
 
34
- if (leafPaths.length === 0) return []
29
+ function makeBarrel(dirPath: string, exports: Array<ExportNode>): FileNode {
30
+ return createFile({
31
+ baseName: BARREL_FILENAME,
32
+ path: `${dirPath}${BARREL_SUFFIX}`,
33
+ exports,
34
+ sources: [],
35
+ imports: [],
36
+ })
37
+ }
35
38
 
36
- const barrelPath = `${treeNode.path}/${BARREL_FILENAME}`
37
- const exports: ReturnType<typeof createExport>[] = []
39
+ type LeafContext = {
40
+ dirPath: string
41
+ leafPath: string
42
+ sourceFile: FileNode | undefined
43
+ }
38
44
 
39
- for (const filePath of leafPaths) {
40
- const sourceFile = sourceFiles.find((f) => f.path === filePath)
41
- // Skip files whose sources all have isIndexable: false (e.g. internal injected files)
42
- if (sourceFile && sourceFile.sources.length > 0 && sourceFile.sources.every((s) => !s.isIndexable)) {
43
- continue
44
- }
45
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
45
+ type LeafStrategy = (ctx: LeafContext) => Array<ExportNode>
46
+
47
+ function hasOnlyNonIndexableSources(sources: ReadonlyArray<SourceNode>): boolean {
48
+ if (sources.length === 0) return false
49
+ for (const source of sources) {
50
+ if (source.isIndexable) return false
46
51
  }
52
+ return true
53
+ }
47
54
 
48
- if (exports.length === 0) return []
49
-
50
- return [
51
- createFile({
52
- baseName: BARREL_FILENAME,
53
- path: barrelPath,
54
- exports,
55
- sources: [],
56
- imports: [],
57
- }),
58
- ]
55
+ function partitionIndexableNames(sources: ReadonlyArray<SourceNode>): Map<boolean, Set<string>> {
56
+ const byTypeOnly = new Map<boolean, Set<string>>([
57
+ [false, new Set()],
58
+ [true, new Set()],
59
+ ])
60
+ for (const source of sources) {
61
+ if (!source.isIndexable || !source.name) continue
62
+ byTypeOnly.get(Boolean(source.isTypeOnly))!.add(source.name)
63
+ }
64
+ return byTypeOnly
59
65
  }
60
66
 
61
- /**
62
- * Generates barrel `FileNode[]` for a given directory tree node using the `'named'` strategy:
63
- * each indexable source in each leaf file gets an individual named `export { name } from '...'`.
64
- */
65
- function getBarrelFilesNamed(treeNode: BuildTree, sourceFiles: ReadonlyArray<FileNode>): Array<FileNode> {
66
- const leafPaths = collectLeafPaths(treeNode).filter((p) => !p.endsWith(`/${BARREL_FILENAME}`))
67
+ const allStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
68
+ if (sourceFile && hasOnlyNonIndexableSources(sourceFile.sources)) return []
69
+ return [createExport({ path: toRelativeModulePath(dirPath, leafPath) })]
70
+ }
67
71
 
68
- if (leafPaths.length === 0) return []
72
+ const namedStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
73
+ const modulePath = toRelativeModulePath(dirPath, leafPath)
69
74
 
70
- const barrelPath = `${treeNode.path}/${BARREL_FILENAME}`
71
- const exports: ReturnType<typeof createExport>[] = []
75
+ if (!sourceFile) return [createExport({ path: modulePath })]
72
76
 
73
- for (const filePath of leafPaths) {
74
- const sourceFile = sourceFiles.find((f) => f.path === filePath)
75
- if (!sourceFile) {
76
- // Fall back to wildcard if the source file is not in our set
77
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
78
- continue
79
- }
77
+ const namesByTypeOnly = partitionIndexableNames(sourceFile.sources)
78
+ const valueNames = namesByTypeOnly.get(false)!
79
+ const typeNames = namesByTypeOnly.get(true)!
80
+
81
+ if (valueNames.size === 0 && typeNames.size === 0) {
82
+ if (sourceFile.sources.length > 0) return []
83
+ return [createExport({ path: modulePath })]
84
+ }
85
+
86
+ const exports: Array<ExportNode> = []
87
+ if (valueNames.size > 0) {
88
+ exports.push(createExport({ name: [...valueNames], path: modulePath }))
89
+ }
90
+ if (typeNames.size > 0) {
91
+ exports.push(createExport({ name: [...typeNames], path: modulePath, isTypeOnly: true }))
92
+ }
93
+ return exports
94
+ }
80
95
 
81
- const indexableSources = sourceFile.sources.filter((s) => s.isIndexable && s.name)
82
- if (indexableSources.length === 0) {
83
- // If the file has explicit sources but none are indexable, skip it entirely.
84
- // Only fall back to wildcard when there are no sources at all (unknown exports).
85
- if (sourceFile.sources.length > 0) continue
86
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
96
+ const LEAF_STRATEGIES: ReadonlyMap<Exclude<BarrelType, 'propagate'>, LeafStrategy> = new Map([
97
+ ['all', allStrategy],
98
+ ['named', namedStrategy],
99
+ ])
100
+
101
+ type LeafWalkParams = {
102
+ sourceFiles: ReadonlyMap<string, FileNode>
103
+ strategy: LeafStrategy
104
+ recursive: boolean
105
+ }
106
+
107
+ /**
108
+ * Single-pass post-order traversal that emits a barrel for each visited directory and
109
+ * returns its leaf paths so parents don't have to re-walk the subtree.
110
+ */
111
+ function walkAllOrNamed(node: BuildTree, params: LeafWalkParams, isRoot: boolean, out: Array<FileNode>): Array<string> {
112
+ const subtreeLeaves: Array<string> = []
113
+
114
+ for (const child of node.children) {
115
+ if (child.isFile) {
116
+ if (!isBarrelPath(child.path)) subtreeLeaves.push(child.path)
87
117
  continue
88
118
  }
89
119
 
90
- const valueNames = indexableSources.filter((s) => !s.isTypeOnly).map((s) => s.name as string)
91
- const typeNames = indexableSources.filter((s) => s.isTypeOnly).map((s) => s.name as string)
92
- const modulePath = toRelativeModulePath(treeNode.path, filePath)
120
+ const childLeaves = walkAllOrNamed(child, params, false, out)
121
+ for (const leaf of childLeaves) subtreeLeaves.push(leaf)
122
+ }
93
123
 
94
- if (valueNames.length > 0) {
95
- exports.push(createExport({ name: valueNames, path: modulePath }))
96
- }
97
- if (typeNames.length > 0) {
98
- exports.push(createExport({ name: typeNames, path: modulePath, isTypeOnly: true }))
99
- }
124
+ // Sub-directory barrels are only emitted when the caller asked for them.
125
+ if (!isRoot && !params.recursive) return subtreeLeaves
126
+
127
+ const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({ dirPath: node.path, leafPath, sourceFile: params.sourceFiles.get(leafPath) }))
128
+
129
+ if (exports.length > 0) {
130
+ out.push(makeBarrel(node.path, exports))
100
131
  }
101
132
 
102
- if (exports.length === 0) return []
103
-
104
- return [
105
- createFile({
106
- baseName: BARREL_FILENAME,
107
- path: barrelPath,
108
- exports,
109
- sources: [],
110
- imports: [],
111
- }),
112
- ]
133
+ return subtreeLeaves
113
134
  }
114
135
 
115
136
  /**
116
- * Generates barrel `FileNode[]` for a given directory tree node using the `'propagate'` strategy:
117
- * like `'all'` but also generates intermediate barrel files for every sub-directory, so that
118
- * consumers can import from any depth.
119
- *
120
- * Leaf barrels export directly from their files; parent barrels export from their sub-barrel files.
137
+ * Emits one barrel per directory: every direct child file is re-exported and every
138
+ * sub-directory is re-exported via its own barrel (recursive by design).
121
139
  */
122
- function getBarrelFilesPropagate(treeNode: BuildTree): Array<FileNode> {
123
- return collectPropagatedBarrels(treeNode)
124
- }
125
-
126
- function collectPropagatedBarrels(node: BuildTree): Array<FileNode> {
127
- const result: Array<FileNode> = []
128
- const barrelExports: ReturnType<typeof createExport>[] = []
140
+ function walkPropagate(node: BuildTree, out: Array<FileNode>): void {
141
+ const exports: Array<ExportNode> = []
129
142
 
130
143
  for (const child of node.children) {
131
144
  if (child.isFile) {
132
- if (!child.path.endsWith(`/${BARREL_FILENAME}`)) {
133
- barrelExports.push(createExport({ path: toRelativeModulePath(node.path, child.path) }))
134
- }
135
- } else {
136
- // Recurse into sub-directory
137
- const subBarrels = collectPropagatedBarrels(child)
138
- result.push(...subBarrels)
139
-
140
- // Export the sub-directory's barrel (not individual files)
141
- const subBarrelPath = `${child.path}/${BARREL_FILENAME}`
142
- barrelExports.push(createExport({ path: toRelativeModulePath(node.path, subBarrelPath) }))
145
+ if (isBarrelPath(child.path)) continue
146
+ exports.push(createExport({ path: toRelativeModulePath(node.path, child.path) }))
147
+ continue
143
148
  }
149
+
150
+ walkPropagate(child, out)
151
+ exports.push(createExport({ path: toRelativeModulePath(node.path, `${child.path}${BARREL_SUFFIX}`) }))
144
152
  }
145
153
 
146
- if (barrelExports.length > 0) {
147
- result.push(
148
- createFile({
149
- baseName: BARREL_FILENAME,
150
- path: `${node.path}/${BARREL_FILENAME}`,
151
- exports: barrelExports,
152
- sources: [],
153
- imports: [],
154
- }),
155
- )
154
+ if (exports.length > 0) {
155
+ out.push(makeBarrel(node.path, exports))
156
156
  }
157
+ }
157
158
 
158
- return result
159
+ type IndexedFiles = {
160
+ /**
161
+ * `path → FileNode` lookup limited to files that participate in barrel generation.
162
+ */
163
+ sourceFiles: ReadonlyMap<string, FileNode>
164
+ /**
165
+ * Original (un-normalized) paths of `sourceFiles`, in input order — used as input for {@link buildTree}.
166
+ */
167
+ paths: ReadonlyArray<string>
159
168
  }
160
169
 
161
- /**
162
- * Collects all leaf (file) paths within a tree node recursively.
163
- */
164
- function collectLeafPaths(node: BuildTree): Array<string> {
165
- if (node.isFile) return [node.path]
166
- return node.children.flatMap((c) => collectLeafPaths(c))
170
+ function indexRelevantFiles(files: ReadonlyArray<FileNode>, outputPath: string): IndexedFiles {
171
+ const outputPrefix = `${outputPath.replaceAll('\\', '/')}/`
172
+ const sourceFiles = new Map<string, FileNode>()
173
+ const paths: Array<string> = []
174
+
175
+ for (const file of files) {
176
+ const normalized = file.path.replaceAll('\\', '/')
177
+ if (!normalized.startsWith(outputPrefix)) continue
178
+ if (isBarrelPath(normalized)) continue
179
+ if (!SOURCE_EXTENSIONS.has(extname(normalized))) continue
180
+
181
+ sourceFiles.set(file.path, file)
182
+ paths.push(file.path)
183
+ }
184
+
185
+ return { sourceFiles, paths }
186
+ }
187
+
188
+ export type GetBarrelFilesParams = {
189
+ /**
190
+ * Absolute path to the directory the barrel(s) should be rooted at.
191
+ * Files outside this directory are ignored.
192
+ */
193
+ outputPath: string
194
+ /**
195
+ * Full set of generated files across all plugins.
196
+ * Used both to discover what to re-export and to read each file's indexable sources.
197
+ */
198
+ files: ReadonlyArray<FileNode>
199
+ /**
200
+ * Re-export style used in the generated barrel(s).
201
+ */
202
+ barrelType: BarrelType
203
+ /**
204
+ * When `true`, also generate a barrel for each sub-directory of `outputPath`.
205
+ * Used by per-plugin barrels so that grouped output (e.g. `petController/`) gets its own `index.ts`.
206
+ *
207
+ * Has no effect for `barrelType: 'propagate'`, which always recurses by design.
208
+ *
209
+ * @default false
210
+ */
211
+ recursive?: boolean
167
212
  }
168
213
 
169
214
  /**
170
- * Generates barrel `FileNode[]` for a directory rooted at `outputPath`, given the full set of
171
- * generated source `files`, using the specified `barrelType` strategy.
215
+ * Generates barrel `FileNode`s for the directory rooted at `outputPath`.
172
216
  *
173
- * Files not located inside `outputPath` are excluded automatically.
174
- *
175
- * @param outputPath Absolute path to the output directory.
176
- * @param files All generated files (across all plugins).
177
- * @param barrelType Barrel generation strategy.
217
+ * Files outside `outputPath`, existing barrel files, and non-source extensions are filtered out
218
+ * before the tree is built.
178
219
  */
179
- export function getBarrelFiles(outputPath: string, files: ReadonlyArray<FileNode>, barrelType: BarrelType): Array<FileNode> {
180
- // Only include files that live inside this outputPath and have a recognised source extension
181
- const relevantFiles = files.filter((f) => {
182
- const normalizedFilePath = f.path.replace(/\\/g, '/')
183
- const normalizedOutputPath = outputPath.replace(/\\/g, '/')
184
- if (!normalizedFilePath.startsWith(normalizedOutputPath + '/')) return false
185
- if (normalizedFilePath.endsWith(`/${BARREL_FILENAME}`)) return false
186
- const dotIndex = normalizedFilePath.lastIndexOf('.')
187
- const ext = dotIndex === -1 ? '' : normalizedFilePath.slice(dotIndex)
188
- return SOURCE_EXTENSIONS.has(ext)
189
- })
220
+ export function getBarrelFiles({ outputPath, files, barrelType, recursive = false }: GetBarrelFilesParams): Array<FileNode> {
221
+ const { sourceFiles, paths } = indexRelevantFiles(files, outputPath)
222
+ if (paths.length === 0) return []
223
+
224
+ const tree = buildTree(outputPath, paths)
225
+ const result: Array<FileNode> = []
190
226
 
191
- if (relevantFiles.length === 0) return []
192
-
193
- const tree = buildTree(
194
- outputPath,
195
- relevantFiles.map((f) => f.path),
196
- )
197
-
198
- switch (barrelType) {
199
- case 'all':
200
- return getBarrelFilesAll(tree, relevantFiles)
201
- case 'named':
202
- return getBarrelFilesNamed(tree, relevantFiles)
203
- case 'propagate':
204
- return getBarrelFilesPropagate(tree)
205
- default:
206
- return []
227
+ if (barrelType === 'propagate') {
228
+ walkPropagate(tree, result)
229
+ return result
207
230
  }
231
+
232
+ const strategy = LEAF_STRATEGIES.get(barrelType)
233
+ if (!strategy) return result
234
+
235
+ walkAllOrNamed(tree, { sourceFiles, strategy, recursive }, true, result)
236
+ return result
208
237
  }
@@ -0,0 +1,20 @@
1
+ import type { Config, NormalizedPlugin } from '@kubb/core'
2
+ import type { BarrelType } from '../types.ts'
3
+
4
+ const DEFAULT_BARREL_TYPE: BarrelType = 'named'
5
+
6
+ /**
7
+ * Resolves the effective barrel style for a single plugin: explicit plugin option →
8
+ * root config option → `'named'` default. Returns `false` when barrel generation is disabled.
9
+ */
10
+ export function resolvePluginBarrelType(plugin: NormalizedPlugin, config: Config): BarrelType | false {
11
+ return plugin.options.output?.barrelType ?? config.output.barrelType ?? DEFAULT_BARREL_TYPE
12
+ }
13
+
14
+ /**
15
+ * Resolves the effective barrel style for the root `index.ts`: root config option → `'named'` default.
16
+ * Returns `false` when the root barrel is disabled.
17
+ */
18
+ export function resolveRootBarrelType(config: Config): BarrelType | false {
19
+ return config.output.barrelType ?? DEFAULT_BARREL_TYPE
20
+ }
@@ -1,67 +0,0 @@
1
- import { posix } from 'node:path'
2
-
3
- /**
4
- * A node in a directory tree used to compute barrel file exports.
5
- *
6
- * Each `TreeNode` represents either a directory or a file entry.
7
- * Directory nodes have `children`; file nodes have an empty `children` array.
8
- */
9
- export type BuildTree = {
10
- /**
11
- * Absolute path of the directory (root of this subtree) or file.
12
- */
13
- path: string
14
- /**
15
- * Child nodes (sub-directories and files) within this directory.
16
- */
17
- children: Array<BuildTree>
18
- /**
19
- * `true` when this node represents a file (leaf node).
20
- */
21
- isFile: boolean
22
- }
23
-
24
- /**
25
- * Builds a `TreeNode` directory tree from a list of absolute file paths.
26
- *
27
- * All `filePaths` must be inside `rootPath`. Paths that are outside
28
- * the root or that equal the root are silently ignored.
29
- *
30
- * @example
31
- * ```ts
32
- * const tree = buildTree('/src/gen/types', [
33
- * '/src/gen/types/pet.ts',
34
- * '/src/gen/types/user.ts',
35
- * '/src/gen/types/pets/listPets.ts',
36
- * ])
37
- * ```
38
- */
39
- export function buildTree(rootPath: string, filePaths: ReadonlyArray<string>): BuildTree {
40
- const root: BuildTree = { path: rootPath, children: [], isFile: false }
41
-
42
- for (const filePath of filePaths) {
43
- // Only include files inside rootPath
44
- if (!filePath.startsWith(rootPath + posix.sep) && !filePath.startsWith(rootPath + '/')) {
45
- continue
46
- }
47
-
48
- const relative = filePath.slice(rootPath.length).replace(/^\//g, '').replace(/^\\/g, '')
49
- const parts = relative.split(/[/\\]/).filter(Boolean)
50
-
51
- let current = root
52
- for (let i = 0; i < parts.length; i++) {
53
- const isLast = i === parts.length - 1
54
- const part = parts[i]!
55
- const childPath = `${current.path}/${part}`
56
-
57
- let child = current.children.find((c) => c.path === childPath)
58
- if (!child) {
59
- child = { path: childPath, children: [], isFile: isLast }
60
- current.children.push(child)
61
- }
62
- current = child
63
- }
64
- }
65
-
66
- return root
67
- }