@kubb/plugin-barrel 5.0.0-beta.51
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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/chunk-C0LytTxp.js +8 -0
- package/dist/index.cjs +410 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +386 -0
- package/dist/index.js.map +1 -0
- package/extension.yaml +197 -0
- package/package.json +69 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +162 -0
- package/src/types.ts +54 -0
- package/src/utils.ts +258 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { extname, resolve } from 'node:path'
|
|
2
|
+
import { createExport, createFile } from '@kubb/ast'
|
|
3
|
+
import type { ExportNode, FileNode, SourceNode } from '@kubb/ast'
|
|
4
|
+
import type { Config, NormalizedPlugin } from '@kubb/core'
|
|
5
|
+
import { type BuildTree, buildTree, toPosixPath } from '@internals/utils'
|
|
6
|
+
import type { BarrelType } from './types.ts'
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
|
|
9
|
+
const BARREL_SUFFIX = `/index.ts`
|
|
10
|
+
|
|
11
|
+
function toRelativeModulePath(fromDir: string, filePath: string): string {
|
|
12
|
+
return `./${filePath.slice(fromDir.length + 1)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isBarrelPath(path: string): boolean {
|
|
16
|
+
return path.endsWith(BARREL_SUFFIX)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeBarrel(dirPath: string, exports: Array<ExportNode>): FileNode {
|
|
20
|
+
return createFile({
|
|
21
|
+
baseName: 'index.ts',
|
|
22
|
+
path: `${dirPath}${BARREL_SUFFIX}`,
|
|
23
|
+
exports,
|
|
24
|
+
sources: [],
|
|
25
|
+
imports: [],
|
|
26
|
+
// Default to no banner/footer. The barrel plugin resolves a configured plugin
|
|
27
|
+
// banner/footer (with isBarrel: true) afterwards, so a `banner` function can
|
|
28
|
+
// decide per file whether a barrel should carry a directive like "use server".
|
|
29
|
+
banner: undefined,
|
|
30
|
+
footer: undefined,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type LeafContext = {
|
|
35
|
+
dirPath: string
|
|
36
|
+
leafPath: string
|
|
37
|
+
sourceFile: FileNode | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type LeafStrategy = (ctx: LeafContext) => Array<ExportNode>
|
|
41
|
+
|
|
42
|
+
function hasOnlyNonIndexableSources(sources: ReadonlyArray<SourceNode>): boolean {
|
|
43
|
+
if (sources.length === 0) return false
|
|
44
|
+
for (const source of sources) {
|
|
45
|
+
if (source.isIndexable) return false
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function partitionIndexableNames(sources: ReadonlyArray<SourceNode>): Map<boolean, Set<string>> {
|
|
51
|
+
const byTypeOnly = new Map<boolean, Set<string>>([
|
|
52
|
+
[false, new Set()],
|
|
53
|
+
[true, new Set()],
|
|
54
|
+
])
|
|
55
|
+
for (const source of sources) {
|
|
56
|
+
if (!source.isIndexable || !source.name) continue
|
|
57
|
+
byTypeOnly.get(Boolean(source.isTypeOnly))!.add(source.name)
|
|
58
|
+
}
|
|
59
|
+
return byTypeOnly
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const allStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
|
|
63
|
+
if (sourceFile && hasOnlyNonIndexableSources(sourceFile.sources)) return []
|
|
64
|
+
return [createExport({ path: toRelativeModulePath(dirPath, leafPath) })]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const namedStrategy: LeafStrategy = ({ dirPath, leafPath, sourceFile }) => {
|
|
68
|
+
const modulePath = toRelativeModulePath(dirPath, leafPath)
|
|
69
|
+
|
|
70
|
+
if (!sourceFile) return [createExport({ path: modulePath })]
|
|
71
|
+
|
|
72
|
+
const namesByTypeOnly = partitionIndexableNames(sourceFile.sources)
|
|
73
|
+
const valueNames = namesByTypeOnly.get(false)!
|
|
74
|
+
const typeNames = namesByTypeOnly.get(true)!
|
|
75
|
+
|
|
76
|
+
if (valueNames.size === 0 && typeNames.size === 0) {
|
|
77
|
+
if (sourceFile.sources.length > 0) return []
|
|
78
|
+
return [createExport({ path: modulePath })]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const exports: Array<ExportNode> = []
|
|
82
|
+
if (valueNames.size > 0) {
|
|
83
|
+
exports.push(createExport({ name: [...valueNames].sort(), path: modulePath }))
|
|
84
|
+
}
|
|
85
|
+
if (typeNames.size > 0) {
|
|
86
|
+
exports.push(createExport({ name: [...typeNames].sort(), path: modulePath, isTypeOnly: true }))
|
|
87
|
+
}
|
|
88
|
+
return exports
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const LEAF_STRATEGIES: ReadonlyMap<BarrelType, LeafStrategy> = new Map([
|
|
92
|
+
['all', allStrategy],
|
|
93
|
+
['named', namedStrategy],
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
type LeafWalkParams = {
|
|
97
|
+
sourceFiles: ReadonlyMap<string, FileNode>
|
|
98
|
+
strategy: LeafStrategy
|
|
99
|
+
recursive: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Post-order walk that yields a barrel per visited directory.
|
|
104
|
+
* Returns the list of leaf file paths collected in this subtree (used by the parent call).
|
|
105
|
+
*/
|
|
106
|
+
function* walkAllOrNamed(node: BuildTree, params: LeafWalkParams, isRoot: boolean): Generator<FileNode, Array<string>> {
|
|
107
|
+
const subtreeLeaves: Array<string> = []
|
|
108
|
+
|
|
109
|
+
for (const child of node.children) {
|
|
110
|
+
if (child.isFile) {
|
|
111
|
+
if (!isBarrelPath(child.path)) subtreeLeaves.push(child.path)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const childLeaves = yield* walkAllOrNamed(child, params, false)
|
|
116
|
+
for (const leaf of childLeaves) subtreeLeaves.push(leaf)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isRoot && !params.recursive) return subtreeLeaves
|
|
120
|
+
|
|
121
|
+
const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({ dirPath: node.path, leafPath, sourceFile: params.sourceFiles.get(leafPath) ?? null }))
|
|
122
|
+
|
|
123
|
+
if (exports.length > 0) {
|
|
124
|
+
yield makeBarrel(node.path, exports)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return subtreeLeaves
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recursive walk that yields one barrel per directory, re-exporting files and sub-barrels.
|
|
132
|
+
* Used when nested: true.
|
|
133
|
+
*/
|
|
134
|
+
function* walkNested(node: BuildTree): Generator<FileNode> {
|
|
135
|
+
const exports: Array<ExportNode> = []
|
|
136
|
+
|
|
137
|
+
for (const child of node.children) {
|
|
138
|
+
if (child.isFile) {
|
|
139
|
+
if (isBarrelPath(child.path)) continue
|
|
140
|
+
exports.push(createExport({ path: toRelativeModulePath(node.path, child.path) }))
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
yield* walkNested(child)
|
|
145
|
+
exports.push(createExport({ path: toRelativeModulePath(node.path, `${child.path}${BARREL_SUFFIX}`) }))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (exports.length > 0) {
|
|
149
|
+
yield makeBarrel(node.path, exports)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type IndexedFiles = {
|
|
154
|
+
sourceFiles: ReadonlyMap<string, FileNode>
|
|
155
|
+
paths: ReadonlyArray<string>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function indexRelevantFiles(files: ReadonlyArray<FileNode>, outputPath: string): IndexedFiles {
|
|
159
|
+
const outputPrefix = `${toPosixPath(outputPath)}/`
|
|
160
|
+
const sourceFiles = new Map<string, FileNode>()
|
|
161
|
+
const paths: Array<string> = []
|
|
162
|
+
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const normalized = toPosixPath(file.path)
|
|
165
|
+
if (!normalized.startsWith(outputPrefix)) continue
|
|
166
|
+
if (isBarrelPath(normalized)) continue
|
|
167
|
+
if (!SOURCE_EXTENSIONS.has(extname(normalized))) continue
|
|
168
|
+
|
|
169
|
+
sourceFiles.set(normalized, file)
|
|
170
|
+
paths.push(normalized)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { sourceFiles, paths }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type GetBarrelFilesParams = {
|
|
177
|
+
/**
|
|
178
|
+
* Absolute directory the barrel(s) should be rooted at.
|
|
179
|
+
* Only files living under this path are considered.
|
|
180
|
+
*/
|
|
181
|
+
outputPath: string
|
|
182
|
+
/**
|
|
183
|
+
* Pool of generated files to scan for indexable sources.
|
|
184
|
+
*/
|
|
185
|
+
files: ReadonlyArray<FileNode>
|
|
186
|
+
/**
|
|
187
|
+
* Export strategy used when emitting each barrel.
|
|
188
|
+
* - `'all'` re-exports the whole module (`export * from './x'`)
|
|
189
|
+
* - `'named'` re-exports only the indexable named symbols
|
|
190
|
+
*/
|
|
191
|
+
barrelType: BarrelType
|
|
192
|
+
/**
|
|
193
|
+
* Generate an `index.ts` in every sub-directory, each re-exporting only what's directly inside it (hierarchical).
|
|
194
|
+
* When false, uses flat generation strategy with optional recursive subdirectory barrels.
|
|
195
|
+
*
|
|
196
|
+
* @default false
|
|
197
|
+
*/
|
|
198
|
+
nested?: boolean
|
|
199
|
+
/**
|
|
200
|
+
* Also generate a barrel for each sub-directory when nested is false.
|
|
201
|
+
* No effect when nested is true (always generates hierarchical structure).
|
|
202
|
+
*
|
|
203
|
+
* @default false
|
|
204
|
+
*/
|
|
205
|
+
recursive?: boolean
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Yields barrel `FileNode`s for the directory rooted at `outputPath`.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* for (const file of getBarrelFiles({ outputPath, files, barrelType })) {
|
|
214
|
+
* upsertFile(file)
|
|
215
|
+
* }
|
|
216
|
+
* // or collect into an array
|
|
217
|
+
* const barrels = [...getBarrelFiles({ outputPath, files, barrelType })]
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function* getBarrelFiles({ outputPath, files, barrelType, nested = false, recursive = false }: GetBarrelFilesParams): Generator<FileNode> {
|
|
221
|
+
const { sourceFiles, paths } = indexRelevantFiles(files, outputPath)
|
|
222
|
+
if (paths.length === 0) return
|
|
223
|
+
|
|
224
|
+
const tree = buildTree(outputPath, paths)
|
|
225
|
+
|
|
226
|
+
if (nested) {
|
|
227
|
+
yield* walkNested(tree)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const strategy = LEAF_STRATEGIES.get(barrelType)
|
|
232
|
+
if (!strategy) return
|
|
233
|
+
|
|
234
|
+
yield* walkAllOrNamed(tree, { sourceFiles, strategy, recursive }, true)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Builds a POSIX-normalized prefix for a plugin's output. A directory output gets a trailing `/`,
|
|
239
|
+
* while a `mode: 'file'` output (the path is the file itself) gets the exact path with no trailing `/`.
|
|
240
|
+
*
|
|
241
|
+
* Used to detect (and later exclude) files generated by plugins that opted out of the root barrel.
|
|
242
|
+
*/
|
|
243
|
+
export function getPluginOutputPrefix(plugin: NormalizedPlugin, config: Config): string {
|
|
244
|
+
const resolved = toPosixPath(resolve(config.root, config.output.path, plugin.options.output.path))
|
|
245
|
+
return plugin.options.output.mode === 'file' ? resolved : `${resolved}/`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns `true` when `filePath` lives under any of the given excluded prefixes. A prefix with a
|
|
250
|
+
* trailing `/` matches a directory subtree, and a prefix without one matches that exact file
|
|
251
|
+
* (used for `mode: 'file'` outputs).
|
|
252
|
+
*
|
|
253
|
+
* Both sides are POSIX-normalized so Windows backslash paths match correctly.
|
|
254
|
+
*/
|
|
255
|
+
export function isExcludedPath(filePath: string, prefixes: ReadonlySet<string>): boolean {
|
|
256
|
+
const normalized = toPosixPath(filePath)
|
|
257
|
+
return prefixes.values().some((prefix) => (prefix.endsWith('/') ? normalized.startsWith(prefix) : normalized === prefix))
|
|
258
|
+
}
|