@kubb/middleware-barrel 5.0.0-alpha.63 → 5.0.0-alpha.65

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,10 +1,12 @@
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
12
  * Derives a relative module specifier from `filePath` relative to `fromDir`.
@@ -17,154 +19,170 @@ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
17
19
  * ```
18
20
  */
19
21
  function toRelativeModulePath(fromDir: string, filePath: string): string {
20
- const relative = filePath.slice(fromDir.length).replace(/^[/\\]/g, '')
21
- return `./${relative}`
22
+ return `./${filePath.slice(fromDir.length + 1)}`
22
23
  }
23
24
 
24
- type BarrelFilesParams = {
25
- treeNode: BuildTree
26
- sourceFiles: ReadonlyArray<FileNode>
27
- recursive?: boolean
25
+ function isBarrelPath(path: string): boolean {
26
+ return path.endsWith(BARREL_SUFFIX)
28
27
  }
29
28
 
30
- function getBarrelFilesAll({ treeNode, sourceFiles, recursive = false }: BarrelFilesParams): Array<FileNode> {
31
- const leafPaths = collectLeafPaths(treeNode).filter((p) => !p.endsWith(`/${BARREL_FILENAME}`))
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
+ }
32
38
 
33
- if (leafPaths.length === 0) return []
39
+ type LeafContext = {
40
+ dirPath: string
41
+ leafPath: string
42
+ sourceFile: FileNode | undefined
43
+ }
34
44
 
35
- const exports: ReturnType<typeof createExport>[] = []
45
+ type LeafStrategy = (ctx: LeafContext) => Array<ExportNode>
36
46
 
37
- for (const filePath of leafPaths) {
38
- const sourceFile = sourceFiles.find((f) => f.path === filePath)
39
- if (sourceFile && sourceFile.sources.length > 0 && sourceFile.sources.every((s) => !s.isIndexable)) {
40
- continue
41
- }
42
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
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
43
51
  }
52
+ return true
53
+ }
44
54
 
45
- const result: Array<FileNode> = []
46
-
47
- if (recursive) {
48
- for (const child of treeNode.children) {
49
- if (!child.isFile) {
50
- result.push(...getBarrelFilesAll({ treeNode: child, sourceFiles, recursive: true }))
51
- }
52
- }
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)
53
63
  }
64
+ return byTypeOnly
65
+ }
54
66
 
55
- if (exports.length === 0) return result
67
+ const allStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
68
+ if (sourceFile && hasOnlyNonIndexableSources(sourceFile.sources)) return []
69
+ return [createExport({ path: toRelativeModulePath(dirPath, leafPath) })]
70
+ }
56
71
 
57
- result.push(
58
- createFile({
59
- baseName: BARREL_FILENAME,
60
- path: `${treeNode.path}/${BARREL_FILENAME}`,
61
- exports,
62
- sources: [],
63
- imports: [],
64
- }),
65
- )
72
+ const namedStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
73
+ const modulePath = toRelativeModulePath(dirPath, leafPath)
66
74
 
67
- return result
68
- }
75
+ if (!sourceFile) return [createExport({ path: modulePath })]
69
76
 
70
- function getBarrelFilesNamed({ treeNode, sourceFiles, recursive = false }: BarrelFilesParams): Array<FileNode> {
71
- const leafPaths = collectLeafPaths(treeNode).filter((p) => !p.endsWith(`/${BARREL_FILENAME}`))
77
+ const namesByTypeOnly = partitionIndexableNames(sourceFile.sources)
78
+ const valueNames = namesByTypeOnly.get(false)!
79
+ const typeNames = namesByTypeOnly.get(true)!
72
80
 
73
- if (leafPaths.length === 0) return []
81
+ if (valueNames.size === 0 && typeNames.size === 0) {
82
+ if (sourceFile.sources.length > 0) return []
83
+ return [createExport({ path: modulePath })]
84
+ }
74
85
 
75
- const exports: ReturnType<typeof createExport>[] = []
86
+ const exports: Array<ExportNode> = []
87
+ if (valueNames.size > 0) {
88
+ exports.push(createExport({ name: [...valueNames].sort(), path: modulePath }))
89
+ }
90
+ if (typeNames.size > 0) {
91
+ exports.push(createExport({ name: [...typeNames].sort(), path: modulePath, isTypeOnly: true }))
92
+ }
93
+ return exports
94
+ }
76
95
 
77
- for (const filePath of leafPaths) {
78
- const sourceFile = sourceFiles.find((f) => f.path === filePath)
79
- if (!sourceFile) {
80
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
81
- continue
82
- }
96
+ const LEAF_STRATEGIES: ReadonlyMap<Exclude<BarrelType, 'propagate'>, LeafStrategy> = new Map([
97
+ ['all', allStrategy],
98
+ ['named', namedStrategy],
99
+ ])
83
100
 
84
- const indexableSources = sourceFile.sources.filter((s) => s.isIndexable && s.name)
85
- if (indexableSources.length === 0) {
86
- if (sourceFile.sources.length > 0) continue
87
- exports.push(createExport({ path: toRelativeModulePath(treeNode.path, filePath) }))
88
- continue
89
- }
101
+ type LeafWalkParams = {
102
+ sourceFiles: ReadonlyMap<string, FileNode>
103
+ strategy: LeafStrategy
104
+ recursive: boolean
105
+ }
90
106
 
91
- const valueNames = indexableSources.filter((s) => !s.isTypeOnly).map((s) => s.name as string)
92
- const typeNames = indexableSources.filter((s) => s.isTypeOnly).map((s) => s.name as string)
93
- const modulePath = toRelativeModulePath(treeNode.path, filePath)
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> = []
94
113
 
95
- if (valueNames.length > 0) {
96
- exports.push(createExport({ name: valueNames, path: modulePath }))
97
- }
98
- if (typeNames.length > 0) {
99
- exports.push(createExport({ name: typeNames, path: modulePath, isTypeOnly: true }))
114
+ for (const child of node.children) {
115
+ if (child.isFile) {
116
+ if (!isBarrelPath(child.path)) subtreeLeaves.push(child.path)
117
+ continue
100
118
  }
119
+
120
+ const childLeaves = walkAllOrNamed(child, params, false, out)
121
+ for (const leaf of childLeaves) subtreeLeaves.push(leaf)
101
122
  }
102
123
 
103
- const result: Array<FileNode> = []
124
+ // Sub-directory barrels are only emitted when the caller asked for them.
125
+ if (!isRoot && !params.recursive) return subtreeLeaves
104
126
 
105
- if (recursive) {
106
- for (const child of treeNode.children) {
107
- if (!child.isFile) {
108
- result.push(...getBarrelFilesNamed({ treeNode: child, sourceFiles, recursive: true }))
109
- }
110
- }
111
- }
127
+ const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({ dirPath: node.path, leafPath, sourceFile: params.sourceFiles.get(leafPath) }))
112
128
 
113
129
  if (exports.length > 0) {
114
- result.push(
115
- createFile({
116
- baseName: BARREL_FILENAME,
117
- path: `${treeNode.path}/${BARREL_FILENAME}`,
118
- exports,
119
- sources: [],
120
- imports: [],
121
- }),
122
- )
130
+ out.push(makeBarrel(node.path, exports))
123
131
  }
124
132
 
125
- return result
126
- }
127
-
128
- function getBarrelFilesPropagate({ treeNode }: Pick<BarrelFilesParams, 'treeNode'>): Array<FileNode> {
129
- return collectPropagatedBarrels(treeNode)
133
+ return subtreeLeaves
130
134
  }
131
135
 
132
- function collectPropagatedBarrels(node: BuildTree): Array<FileNode> {
133
- const result: Array<FileNode> = []
134
- const barrelExports: ReturnType<typeof createExport>[] = []
136
+ /**
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).
139
+ */
140
+ function walkPropagate(node: BuildTree, out: Array<FileNode>): void {
141
+ const exports: Array<ExportNode> = []
135
142
 
136
143
  for (const child of node.children) {
137
144
  if (child.isFile) {
138
- if (!child.path.endsWith(`/${BARREL_FILENAME}`)) {
139
- barrelExports.push(createExport({ path: toRelativeModulePath(node.path, child.path) }))
140
- }
141
- } else {
142
- const subBarrels = collectPropagatedBarrels(child)
143
- result.push(...subBarrels)
144
-
145
- const subBarrelPath = `${child.path}/${BARREL_FILENAME}`
146
- 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
147
148
  }
149
+
150
+ walkPropagate(child, out)
151
+ exports.push(createExport({ path: toRelativeModulePath(node.path, `${child.path}${BARREL_SUFFIX}`) }))
148
152
  }
149
153
 
150
- if (barrelExports.length > 0) {
151
- result.push(
152
- createFile({
153
- baseName: BARREL_FILENAME,
154
- path: `${node.path}/${BARREL_FILENAME}`,
155
- exports: barrelExports,
156
- sources: [],
157
- imports: [],
158
- }),
159
- )
154
+ if (exports.length > 0) {
155
+ out.push(makeBarrel(node.path, exports))
160
156
  }
157
+ }
161
158
 
162
- 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>
163
168
  }
164
169
 
165
- function collectLeafPaths(node: BuildTree): Array<string> {
166
- if (node.isFile) return [node.path]
167
- 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 }
168
186
  }
169
187
 
170
188
  export type GetBarrelFilesParams = {
@@ -200,31 +218,20 @@ export type GetBarrelFilesParams = {
200
218
  * before the tree is built.
201
219
  */
202
220
  export function getBarrelFiles({ outputPath, files, barrelType, recursive = false }: GetBarrelFilesParams): Array<FileNode> {
203
- const relevantFiles = files.filter((f) => {
204
- const normalizedFilePath = f.path.replace(/\\/g, '/')
205
- const normalizedOutputPath = outputPath.replace(/\\/g, '/')
206
- if (!normalizedFilePath.startsWith(normalizedOutputPath + '/')) return false
207
- if (normalizedFilePath.endsWith(`/${BARREL_FILENAME}`)) return false
208
- const dotIndex = normalizedFilePath.lastIndexOf('.')
209
- const ext = dotIndex === -1 ? '' : normalizedFilePath.slice(dotIndex)
210
- return SOURCE_EXTENSIONS.has(ext)
211
- })
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> = []
212
226
 
213
- if (relevantFiles.length === 0) return []
214
-
215
- const tree = buildTree(
216
- outputPath,
217
- relevantFiles.map((f) => f.path),
218
- )
219
-
220
- switch (barrelType) {
221
- case 'all':
222
- return getBarrelFilesAll({ treeNode: tree, sourceFiles: relevantFiles, recursive })
223
- case 'named':
224
- return getBarrelFilesNamed({ treeNode: tree, sourceFiles: relevantFiles, recursive })
225
- case 'propagate':
226
- return getBarrelFilesPropagate({ treeNode: tree })
227
- default:
228
- return []
227
+ if (barrelType === 'propagate') {
228
+ walkPropagate(tree, result)
229
+ return result
229
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
230
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,63 +0,0 @@
1
- import { posix } from 'node:path'
2
-
3
- /**
4
- * A node in the directory tree used to compute barrel file exports.
5
- * Either represents a directory (with `children`) or a file (`isFile: true`, empty `children`).
6
- */
7
- export type BuildTree = {
8
- /**
9
- * Absolute filesystem path of this directory or file.
10
- */
11
- path: string
12
- /**
13
- * Sub-directories and files contained within this directory.
14
- * Always empty for file nodes.
15
- */
16
- children: Array<BuildTree>
17
- /**
18
- * `true` when this node represents a file (leaf), `false` for directory nodes.
19
- */
20
- isFile: boolean
21
- }
22
-
23
- /**
24
- * Builds a directory tree rooted at `rootPath` from a list of absolute file paths.
25
- * Paths outside `rootPath` are silently ignored.
26
- *
27
- * @example
28
- * ```ts
29
- * buildTree('/src/gen/types', [
30
- * '/src/gen/types/pet.ts',
31
- * '/src/gen/types/pets/listPets.ts',
32
- * ])
33
- * ```
34
- */
35
- export function buildTree(rootPath: string, filePaths: ReadonlyArray<string>): BuildTree {
36
- const root: BuildTree = { path: rootPath, children: [], isFile: false }
37
-
38
- for (const filePath of filePaths) {
39
- // Only include files inside rootPath
40
- if (!filePath.startsWith(rootPath + posix.sep) && !filePath.startsWith(rootPath + '/')) {
41
- continue
42
- }
43
-
44
- const relative = filePath.slice(rootPath.length).replace(/^\//g, '').replace(/^\\/g, '')
45
- const parts = relative.split(/[/\\]/).filter(Boolean)
46
-
47
- let current = root
48
- for (let i = 0; i < parts.length; i++) {
49
- const isLast = i === parts.length - 1
50
- const part = parts[i]!
51
- const childPath = `${current.path}/${part}`
52
-
53
- let child = current.children.find((c) => c.path === childPath)
54
- if (!child) {
55
- child = { path: childPath, children: [], isFile: isLast }
56
- current.children.push(child)
57
- }
58
- current = child
59
- }
60
- }
61
-
62
- return root
63
- }