@kubb/middleware-barrel 5.0.0-beta.3 → 5.0.0-beta.30

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/src/middleware.ts CHANGED
@@ -1,10 +1,34 @@
1
1
  import path from 'node:path'
2
2
  import { resolve } from 'node:path'
3
+ import type { FileNode } from '@kubb/ast'
3
4
  import { defineMiddleware } from '@kubb/core'
4
- import type { Middleware } from '@kubb/core'
5
+ import type { Config, Middleware, NormalizedPlugin } from '@kubb/core'
5
6
  import type { BarrelConfig, PluginBarrelConfig } from './types.ts'
6
- import { getPluginOutputPrefix, isExcludedPath } from './utils/excludedPaths.ts'
7
- import { getBarrelFiles } from './utils/getBarrelFiles.ts'
7
+ import { getBarrelFiles, getPluginOutputPrefix, isExcludedPath } from './utils.ts'
8
+
9
+ /**
10
+ * Applies a plugin's configured `output.banner`/`footer` to a barrel file, flagged as `isBarrel`.
11
+ *
12
+ * Resolves through the plugin's own resolver, and only when the plugin explicitly sets a
13
+ * banner/footer — so barrels stay banner-free by default and never inherit the implicit
14
+ * "Generated by Kubb" notice.
15
+ */
16
+ function withBarrelBannerFooter({ file, plugin, config }: { file: FileNode; plugin: NormalizedPlugin; config: Config }): FileNode {
17
+ const output = plugin.options?.output
18
+ const resolver = plugin.resolver
19
+ if (!resolver) return file
20
+
21
+ const hasBanner = output?.banner !== undefined
22
+ const hasFooter = output?.footer !== undefined
23
+ if (!hasBanner && !hasFooter) return file
24
+
25
+ const context = { output, config, file: { path: file.path, baseName: file.baseName, isBarrel: true } }
26
+ return {
27
+ ...file,
28
+ banner: hasBanner ? resolver.resolveBanner(undefined, context) : file.banner,
29
+ footer: hasFooter ? resolver.resolveFooter(undefined, context) : file.footer,
30
+ }
31
+ }
8
32
 
9
33
  declare global {
10
34
  namespace Kubb {
@@ -38,17 +62,29 @@ declare global {
38
62
  }
39
63
 
40
64
  /**
41
- * Generates `index.ts` barrel files for each plugin and a root barrel at `config.output.path/index.ts`.
65
+ * Canonical middleware name for `@kubb/middleware-barrel`. Used for driver
66
+ * lookups.
67
+ */
68
+ export const middlewareBarrelName = 'middleware-barrel' satisfies Middleware['name']
69
+
70
+ /**
71
+ * Generates an `index.ts` for every plugin output directory and one root
72
+ * barrel at `config.output.path/index.ts` after the build completes. Ships
73
+ * with Kubb and is registered by default in `defineConfig`.
42
74
  *
43
- * Each plugin inherits `output.barrel` from `config.output.barrel` (defaults to `{ type: 'named' }`).
44
- * Set `barrel: false` on a plugin to disable its barrel and exclude it from the root barrel.
75
+ * Each plugin inherits `output.barrel` from `config.output.barrel` (which
76
+ * defaults to `{ type: 'named' }`). Set `barrel: false` on a plugin to skip
77
+ * its barrel and also exclude its files from the root barrel.
45
78
  *
46
79
  * @example
47
80
  * ```ts
48
81
  * import { defineConfig } from '@kubb/core'
49
82
  * import { middlewareBarrel } from '@kubb/middleware-barrel'
83
+ * import { pluginTs } from '@kubb/plugin-ts'
84
+ * import { pluginZod } from '@kubb/plugin-zod'
50
85
  *
51
86
  * export default defineConfig({
87
+ * input: { path: './petStore.yaml' },
52
88
  * output: { path: 'src/gen', barrel: { type: 'named' } },
53
89
  * plugins: [
54
90
  * pluginTs({ output: { path: 'types', barrel: { type: 'all' } } }),
@@ -58,12 +94,6 @@ declare global {
58
94
  * })
59
95
  * ```
60
96
  */
61
-
62
- /**
63
- * Stable string identifier for the barrel middleware.
64
- */
65
- export const middlewareBarrelName = 'middleware-barrel' satisfies Middleware['name']
66
-
67
97
  export const middlewareBarrel = defineMiddleware(() => {
68
98
  const excludedPrefixes = new Set<string>()
69
99
 
@@ -75,15 +105,12 @@ export const middlewareBarrel = defineMiddleware(() => {
75
105
  const configBarrel = config.output.barrel
76
106
  const defaultBarrel = { type: 'named' } as const
77
107
 
78
- let barrelConfig: PluginBarrelConfig | false
79
- if (pluginBarrel !== undefined) {
80
- barrelConfig = pluginBarrel
81
- } else if (configBarrel !== undefined) {
82
- // Root config barrel doesn't have nested, so we add it
83
- barrelConfig = configBarrel === false ? false : { ...configBarrel, nested: false }
84
- } else {
85
- barrelConfig = defaultBarrel
86
- }
108
+ // Root config barrel doesn't have nested, so we add it
109
+ const barrelConfig: PluginBarrelConfig | false = (() => {
110
+ if (pluginBarrel !== undefined) return pluginBarrel
111
+ if (configBarrel !== undefined) return configBarrel === false ? false : { ...configBarrel, nested: false }
112
+ return defaultBarrel
113
+ })()
87
114
 
88
115
  if (barrelConfig === false) {
89
116
  excludedPrefixes.add(getPluginOutputPrefix(plugin, config))
@@ -99,16 +126,8 @@ export const middlewareBarrel = defineMiddleware(() => {
99
126
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
100
127
  throw new Error('Invalid output path')
101
128
  }
102
- const barrelFiles = getBarrelFiles({
103
- outputPath: target,
104
- files,
105
- barrelType,
106
- nested,
107
- recursive: true,
108
- })
109
-
110
- if (barrelFiles.length > 0) {
111
- upsertFile(...barrelFiles)
129
+ for (const file of getBarrelFiles({ outputPath: target, files, barrelType, nested, recursive: true })) {
130
+ upsertFile(withBarrelBannerFooter({ file, plugin, config }))
112
131
  }
113
132
  },
114
133
  'kubb:plugins:end'({ files, config, upsertFile }) {
@@ -121,14 +140,8 @@ export const middlewareBarrel = defineMiddleware(() => {
121
140
 
122
141
  const barrelType = barrelConfig.type
123
142
 
124
- const rootBarrelFiles = getBarrelFiles({
125
- outputPath: resolve(config.root, config.output.path),
126
- files: filteredFiles,
127
- barrelType,
128
- })
129
-
130
- if (rootBarrelFiles.length > 0) {
131
- upsertFile(...rootBarrelFiles)
143
+ for (const file of getBarrelFiles({ outputPath: resolve(config.root, config.output.path), files: filteredFiles, barrelType })) {
144
+ upsertFile(file)
132
145
  }
133
146
  },
134
147
  },
@@ -1,12 +1,12 @@
1
- import { extname } from 'node:path'
1
+ import { extname, resolve } from 'node:path'
2
2
  import { createExport, createFile } from '@kubb/ast'
3
3
  import type { ExportNode, FileNode, SourceNode } from '@kubb/ast'
4
- import { BARREL_FILENAME } from '../constants.ts'
5
- import type { BarrelType } from '../types.ts'
4
+ import type { Config, NormalizedPlugin } from '@kubb/core'
6
5
  import { type BuildTree, buildTree, toPosixPath } from '@internals/utils'
6
+ import type { BarrelType } from './types.ts'
7
7
 
8
8
  const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
9
- const BARREL_SUFFIX = `/${BARREL_FILENAME}`
9
+ const BARREL_SUFFIX = `/index.ts`
10
10
 
11
11
  function toRelativeModulePath(fromDir: string, filePath: string): string {
12
12
  return `./${filePath.slice(fromDir.length + 1)}`
@@ -18,13 +18,14 @@ function isBarrelPath(path: string): boolean {
18
18
 
19
19
  function makeBarrel(dirPath: string, exports: Array<ExportNode>): FileNode {
20
20
  return createFile({
21
- baseName: BARREL_FILENAME,
21
+ baseName: 'index.ts',
22
22
  path: `${dirPath}${BARREL_SUFFIX}`,
23
23
  exports,
24
24
  sources: [],
25
25
  imports: [],
26
- // Barrel files must never carry a banner or footer: they only re-export
27
- // symbols and adding a directive like "use server" would break consumers.
26
+ // Default to no banner/footer. The middleware 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".
28
29
  banner: undefined,
29
30
  footer: undefined,
30
31
  })
@@ -33,7 +34,7 @@ function makeBarrel(dirPath: string, exports: Array<ExportNode>): FileNode {
33
34
  type LeafContext = {
34
35
  dirPath: string
35
36
  leafPath: string
36
- sourceFile: FileNode | undefined
37
+ sourceFile: FileNode | null
37
38
  }
38
39
 
39
40
  type LeafStrategy = (ctx: LeafContext) => Array<ExportNode>
@@ -99,9 +100,10 @@ type LeafWalkParams = {
99
100
  }
100
101
 
101
102
  /**
102
- * Post-order walk that emits a barrel per visited directory.
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).
103
105
  */
104
- function walkAllOrNamed(node: BuildTree, params: LeafWalkParams, isRoot: boolean, out: Array<FileNode>): Array<string> {
106
+ function* walkAllOrNamed(node: BuildTree, params: LeafWalkParams, isRoot: boolean): Generator<FileNode, Array<string>> {
105
107
  const subtreeLeaves: Array<string> = []
106
108
 
107
109
  for (const child of node.children) {
@@ -110,26 +112,26 @@ function walkAllOrNamed(node: BuildTree, params: LeafWalkParams, isRoot: boolean
110
112
  continue
111
113
  }
112
114
 
113
- const childLeaves = walkAllOrNamed(child, params, false, out)
115
+ const childLeaves = yield* walkAllOrNamed(child, params, false)
114
116
  for (const leaf of childLeaves) subtreeLeaves.push(leaf)
115
117
  }
116
118
 
117
119
  if (!isRoot && !params.recursive) return subtreeLeaves
118
120
 
119
- const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({ dirPath: node.path, leafPath, sourceFile: params.sourceFiles.get(leafPath) }))
121
+ const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({ dirPath: node.path, leafPath, sourceFile: params.sourceFiles.get(leafPath) ?? null }))
120
122
 
121
123
  if (exports.length > 0) {
122
- out.push(makeBarrel(node.path, exports))
124
+ yield makeBarrel(node.path, exports)
123
125
  }
124
126
 
125
127
  return subtreeLeaves
126
128
  }
127
129
 
128
130
  /**
129
- * Recursive walk that emits one barrel per directory, re-exporting files and sub-barrels.
131
+ * Recursive walk that yields one barrel per directory, re-exporting files and sub-barrels.
130
132
  * Used when nested: true.
131
133
  */
132
- function walkNested(node: BuildTree, out: Array<FileNode>): void {
134
+ function* walkNested(node: BuildTree): Generator<FileNode> {
133
135
  const exports: Array<ExportNode> = []
134
136
 
135
137
  for (const child of node.children) {
@@ -139,12 +141,12 @@ function walkNested(node: BuildTree, out: Array<FileNode>): void {
139
141
  continue
140
142
  }
141
143
 
142
- walkNested(child, out)
144
+ yield* walkNested(child)
143
145
  exports.push(createExport({ path: toRelativeModulePath(node.path, `${child.path}${BARREL_SUFFIX}`) }))
144
146
  }
145
147
 
146
148
  if (exports.length > 0) {
147
- out.push(makeBarrel(node.path, exports))
149
+ yield makeBarrel(node.path, exports)
148
150
  }
149
151
  }
150
152
 
@@ -204,24 +206,49 @@ type GetBarrelFilesParams = {
204
206
  }
205
207
 
206
208
  /**
207
- * Generates barrel `FileNode`s for the directory rooted at `outputPath`.
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
+ * ```
208
219
  */
209
- export function getBarrelFiles({ outputPath, files, barrelType, nested = false, recursive = false }: GetBarrelFilesParams): Array<FileNode> {
220
+ export function* getBarrelFiles({ outputPath, files, barrelType, nested = false, recursive = false }: GetBarrelFilesParams): Generator<FileNode> {
210
221
  const { sourceFiles, paths } = indexRelevantFiles(files, outputPath)
211
- if (paths.length === 0) return []
222
+ if (paths.length === 0) return
212
223
 
213
224
  const tree = buildTree(outputPath, paths)
214
- const result: Array<FileNode> = []
215
225
 
216
- // Use nested walk for hierarchical barrel structure
217
226
  if (nested) {
218
- walkNested(tree, result)
219
- return result
227
+ yield* walkNested(tree)
228
+ return
220
229
  }
221
230
 
222
231
  const strategy = LEAF_STRATEGIES.get(barrelType)
223
- if (!strategy) return result
232
+ if (!strategy) return
224
233
 
225
- walkAllOrNamed(tree, { sourceFiles, strategy, recursive }, true, result)
226
- return result
234
+ yield* walkAllOrNamed(tree, { sourceFiles, strategy, recursive }, true)
235
+ }
236
+
237
+ /**
238
+ * Builds a POSIX-normalized prefix for a plugin's output directory, with a trailing `/`.
239
+ *
240
+ * Used to detect (and later exclude) files generated by plugins that opted out of the root barrel.
241
+ */
242
+ export function getPluginOutputPrefix(plugin: NormalizedPlugin, config: Config): string {
243
+ return `${toPosixPath(resolve(config.root, config.output.path, plugin.options.output.path))}/`
244
+ }
245
+
246
+ /**
247
+ * Returns `true` when `filePath` lives under any of the given excluded directory prefixes.
248
+ *
249
+ * Both sides are POSIX-normalized so Windows backslash paths match correctly.
250
+ */
251
+ export function isExcludedPath(filePath: string, prefixes: ReadonlySet<string>): boolean {
252
+ const normalized = toPosixPath(filePath)
253
+ return prefixes.values().some((prefix) => normalized.startsWith(prefix))
227
254
  }
package/src/constants.ts DELETED
@@ -1,4 +0,0 @@
1
- /**
2
- * Full file name for barrel files (with extension).
3
- */
4
- export const BARREL_FILENAME = 'index.ts' as const
@@ -1,22 +0,0 @@
1
- import { resolve } from 'node:path'
2
- import type { Config, NormalizedPlugin } from '@kubb/core'
3
- import { toPosixPath } from '@internals/utils'
4
-
5
- /**
6
- * Builds a POSIX-normalized prefix for a plugin's output directory, with a trailing `/`.
7
- *
8
- * Used to detect (and later exclude) files generated by plugins that opted out of the root barrel.
9
- */
10
- export function getPluginOutputPrefix(plugin: NormalizedPlugin, config: Config): string {
11
- return `${toPosixPath(resolve(config.root, config.output.path, plugin.options.output.path))}/`
12
- }
13
-
14
- /**
15
- * Returns `true` when `filePath` lives under any of the given excluded directory prefixes.
16
- *
17
- * Both sides are POSIX-normalized so Windows backslash paths match correctly.
18
- */
19
- export function isExcludedPath(filePath: string, prefixes: ReadonlySet<string>): boolean {
20
- const normalized = toPosixPath(filePath)
21
- return prefixes.values().some((prefix) => normalized.startsWith(prefix))
22
- }