@kubb/plugin-barrel 5.8.0 → 5.8.1

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/dist/index.js CHANGED
@@ -41,6 +41,15 @@ function isPathInside(path, parent) {
41
41
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
42
42
  }
43
43
  //#endregion
44
+ //#region kubb-extension.json
45
+ var compatibility = {
46
+ "kubb": ">=5.8.0",
47
+ "node": ">=22"
48
+ };
49
+ //#endregion
50
+ //#region package.json
51
+ var version = "5.8.1";
52
+ //#endregion
44
53
  //#region src/utils.ts
45
54
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
46
55
  ".ts",
@@ -50,219 +59,124 @@ const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
50
59
  ]);
51
60
  const BARREL_SUFFIX = `/index.ts`;
52
61
  /**
53
- * Builds a directory tree rooted at `rootPath` from a list of absolute file paths.
54
- * Paths outside `rootPath` are silently ignored. Children are sorted alphabetically
55
- * by path so consumers (barrel exports, propagated indexes) emit a deterministic order.
56
- *
57
- * Both POSIX (`/`) and Windows (`\`) separators are accepted in input paths; emitted node
58
- * paths are always POSIX-normalized so downstream prefix/lookup operations behave the same
59
- * across platforms.
60
- *
61
- * @example
62
- * ```ts
63
- * buildTree('/src/gen/types', [
64
- * '/src/gen/types/pet.ts',
65
- * '/src/gen/types/pets/listPets.ts',
66
- * ])
67
- * ```
62
+ * Orders paths split into segments the way a directory listing sorts them: by name at each level.
68
63
  */
69
- function buildTree(rootPath, filePaths) {
70
- const normalizedRoot = toPosixPath(rootPath);
71
- const root = {
72
- path: normalizedRoot,
73
- children: [],
74
- isFile: false
75
- };
76
- const childIndex = /* @__PURE__ */ new WeakMap();
77
- childIndex.set(root, /* @__PURE__ */ new Map());
78
- const rootPrefix = `${normalizedRoot}/`;
79
- for (const filePath of filePaths) {
80
- const normalized = toPosixPath(filePath);
81
- if (!normalized.startsWith(rootPrefix)) continue;
82
- const parts = normalized.slice(rootPrefix.length).split("/");
83
- let current = root;
84
- const lastIndex = parts.length - 1;
85
- for (const [i, part] of parts.entries()) {
86
- if (!part) continue;
87
- const isLast = i === lastIndex;
88
- const siblings = childIndex.get(current);
89
- let child = siblings.get(part);
90
- if (!child) {
91
- child = {
92
- path: `${current.path}/${part}`,
93
- children: [],
94
- isFile: isLast
95
- };
96
- current.children.push(child);
97
- siblings.set(part, child);
98
- if (!isLast) childIndex.set(child, /* @__PURE__ */ new Map());
99
- }
100
- current = child;
101
- }
102
- }
103
- sortTree(root);
104
- return root;
64
+ function compareSegments(a, b) {
65
+ const index = a.findIndex((segment, i) => segment !== b[i]);
66
+ if (index === -1) return a.length - b.length;
67
+ if (index >= b.length) return 1;
68
+ return a[index] < b[index] ? -1 : 1;
105
69
  }
106
- function sortTree(node) {
107
- if (node.children.length === 0) return;
108
- node.children.sort(compareByPath);
109
- for (const child of node.children) if (!child.isFile) sortTree(child);
110
- }
111
- function compareByPath(a, b) {
112
- return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
113
- }
114
- function toRelativeModulePath({ fromDir, filePath }) {
70
+ function toRelativeModulePath(fromDir, filePath) {
115
71
  return `./${posix.relative(fromDir, filePath)}`;
116
72
  }
117
- function isBarrelPath(path) {
118
- return path.endsWith(BARREL_SUFFIX);
119
- }
120
- function makeBarrel({ dirPath, exports, sourceFiles, reportedCollisions }) {
121
- const names = /* @__PURE__ */ new Map();
122
- const uniqueExports = [];
123
- for (const item of exports) {
73
+ /**
74
+ * Builds the `index.ts` for `dirPath`. Keeps the first export of each name and reports later duplicates once per build.
75
+ */
76
+ function makeBarrel(dirPath, exports, { sourceFiles, reportedCollisions }) {
77
+ const owners = /* @__PURE__ */ new Map();
78
+ const uniqueExports = exports.flatMap((item) => {
124
79
  const itemPath = posix.join(dirPath, item.path);
125
- const itemNames = Array.isArray(item.name) ? item.name : item.name ? [item.name] : sourceFiles.get(itemPath)?.sources.flatMap((source) => source.name ? [source.name] : []);
126
- const uniqueNames = [];
127
- for (const name of itemNames ?? []) {
128
- const first = names.get(name);
80
+ const uniqueNames = (item.name == null ? sourceFiles.get(itemPath)?.sources.flatMap((source) => source.name || []) ?? [] : [item.name].flat()).filter((name) => {
81
+ const first = owners.get(name);
129
82
  if (!first) {
130
- names.set(name, item.path);
131
- uniqueNames.push(name);
132
- continue;
83
+ owners.set(name, item.path);
84
+ return true;
133
85
  }
134
86
  const key = [
135
87
  name,
136
88
  posix.join(dirPath, first),
137
89
  itemPath
138
90
  ].join("\0");
139
- if (reportedCollisions.has(key)) continue;
140
- reportedCollisions.add(key);
141
- Diagnostics.report({
142
- code: Diagnostics.code.barrelDuplicateExport,
143
- severity: "error",
144
- message: `"${name}" is exported by both "${first}" and "${item.path}", so "${dirPath}${BARREL_SUFFIX}" cannot re-export both.`,
145
- help: "Rename one of the colliding declarations, or configure the plugin resolver to produce distinct names."
146
- });
147
- }
148
- if (!itemNames || item.name == null || uniqueNames.length > 0) uniqueExports.push(item.name == null ? item : ast.factory.createExport({
91
+ if (!reportedCollisions.has(key)) {
92
+ reportedCollisions.add(key);
93
+ Diagnostics.report({
94
+ code: Diagnostics.code.barrelDuplicateExport,
95
+ severity: "error",
96
+ message: `"${name}" is exported by both "${first}" and "${item.path}", so "${dirPath}${BARREL_SUFFIX}" cannot re-export both.`,
97
+ help: "Rename one of the colliding declarations, or configure the plugin resolver to produce distinct names."
98
+ });
99
+ }
100
+ return false;
101
+ });
102
+ if (item.name == null) return [item];
103
+ return uniqueNames.length > 0 ? [ast.factory.createExport({
149
104
  ...item,
150
105
  name: Array.isArray(item.name) ? uniqueNames : uniqueNames[0]
151
- }));
152
- }
106
+ })] : [];
107
+ });
153
108
  return ast.factory.createFile({
154
109
  baseName: "index.ts",
155
110
  path: `${dirPath}${BARREL_SUFFIX}`,
156
111
  exports: uniqueExports
157
112
  });
158
113
  }
159
- function indexableNames({ sources, isTypeOnly }) {
114
+ function indexableNames(sources, isTypeOnly) {
160
115
  const names = sources.flatMap((source) => source.isIndexable && source.name && Boolean(source.isTypeOnly) === isTypeOnly ? [source.name] : []);
161
116
  return [...new Set(names)].sort();
162
117
  }
163
- const allStrategy = ({ dirPath, leafPath, sourceFile }) => {
118
+ /**
119
+ * Re-exports the whole module, unless it has sources and none of them is indexable.
120
+ */
121
+ const allStrategy = (path, sourceFile) => {
164
122
  const sources = sourceFile?.sources ?? [];
165
- if (sources.length > 0 && !sources.some((source) => source.isIndexable)) return [];
166
- return [ast.factory.createExport({ path: toRelativeModulePath({
167
- fromDir: dirPath,
168
- filePath: leafPath
169
- }) })];
123
+ return sources.length > 0 && !sources.some((source) => source.isIndexable) ? [] : [ast.factory.createExport({ path })];
170
124
  };
171
- const namedStrategy = ({ dirPath, leafPath, sourceFile }) => {
172
- const modulePath = toRelativeModulePath({
173
- fromDir: dirPath,
174
- filePath: leafPath
175
- });
176
- if (!sourceFile) return [ast.factory.createExport({ path: modulePath })];
177
- const valueNames = indexableNames({
178
- sources: sourceFile.sources,
179
- isTypeOnly: false
180
- });
181
- const typeNames = indexableNames({
182
- sources: sourceFile.sources,
183
- isTypeOnly: true
184
- });
185
- if (valueNames.length === 0 && typeNames.length === 0) {
186
- if (sourceFile.sources.length > 0) return [];
187
- return [ast.factory.createExport({ path: modulePath })];
188
- }
189
- const exports = [];
190
- if (valueNames.length > 0) exports.push(ast.factory.createExport({
125
+ /**
126
+ * Re-exports the indexable names, values and types separately, or the whole module when it has no sources.
127
+ */
128
+ const namedStrategy = (path, sourceFile) => {
129
+ if (!sourceFile?.sources.length) return [ast.factory.createExport({ path })];
130
+ const valueNames = indexableNames(sourceFile.sources, false);
131
+ const typeNames = indexableNames(sourceFile.sources, true);
132
+ return [...valueNames.length > 0 ? [ast.factory.createExport({
191
133
  name: valueNames,
192
- path: modulePath
193
- }));
194
- if (typeNames.length > 0) exports.push(ast.factory.createExport({
134
+ path
135
+ })] : [], ...typeNames.length > 0 ? [ast.factory.createExport({
195
136
  name: typeNames,
196
- path: modulePath,
137
+ path,
197
138
  isTypeOnly: true
198
- }));
199
- return exports;
139
+ })] : []];
200
140
  };
201
141
  /**
202
- * Post-order walk that yields a barrel per visited directory.
203
- * Returns the list of leaf file paths collected in this subtree (used by the parent call).
142
+ * Post-order walk that builds a barrel re-exporting every leaf below the visited directory, for
143
+ * the start node and, with `recursive`, every sub-directory. Also returns the subtree's leaf paths.
204
144
  */
205
- function* walkAllOrNamed(node, params, { isRoot, recursive }) {
206
- const subtreeLeaves = [];
207
- for (const child of node.children) {
208
- if (child.isFile) {
209
- if (!isBarrelPath(child.path)) subtreeLeaves.push(child.path);
210
- continue;
211
- }
212
- const childLeaves = yield* walkAllOrNamed(child, params, {
213
- isRoot: false,
214
- recursive
215
- });
216
- for (const leaf of childLeaves) subtreeLeaves.push(leaf);
217
- }
218
- if (!isRoot && !recursive) return subtreeLeaves;
219
- const exports = subtreeLeaves.flatMap((leafPath) => params.strategy({
220
- dirPath: node.path,
221
- leafPath,
222
- sourceFile: params.sourceFiles.get(leafPath)
223
- }));
224
- if (exports.length > 0) yield makeBarrel({
225
- dirPath: node.path,
226
- exports,
227
- sourceFiles: params.sourceFiles,
228
- reportedCollisions: params.reportedCollisions
145
+ function walkFlat(node, ctx, isRoot = true) {
146
+ const results = [...node.children.values()].map((child) => child.children ? walkFlat(child, ctx, false) : {
147
+ barrels: [],
148
+ leaves: [child.path]
229
149
  });
230
- return subtreeLeaves;
150
+ const barrels = results.flatMap((result) => result.barrels);
151
+ const leaves = results.flatMap((result) => result.leaves);
152
+ const exports = isRoot || ctx.recursive ? leaves.flatMap((leaf) => ctx.leafExports(node.path, leaf)) : [];
153
+ return {
154
+ barrels: exports.length > 0 ? [...barrels, makeBarrel(node.path, exports, ctx)] : barrels,
155
+ leaves
156
+ };
231
157
  }
232
158
  /**
233
- * Recursive walk that yields one barrel per directory, re-exporting files and sub-barrels.
159
+ * Recursive walk that builds one barrel per directory, re-exporting files and sub-barrels.
234
160
  * Used when nested: true. Leaf files honor the barrel `strategy`, so `named` emits explicit
235
161
  * named exports instead of wildcards. Sub-directory barrels are chained with a wildcard
236
- * re-export, which forwards the names the child barrel already curated. Returns whether this
237
- * node yielded a barrel, so a parent never re-exports a sub-directory that produced nothing.
162
+ * re-export, which forwards the names the child barrel already curated. A sub-directory that
163
+ * produced no barrel is never re-exported.
238
164
  */
239
- function* walkNested(node, params) {
240
- const exports = [];
241
- for (const child of node.children) {
242
- if (child.isFile) {
243
- if (isBarrelPath(child.path)) continue;
244
- exports.push(...params.strategy({
245
- dirPath: node.path,
246
- leafPath: child.path,
247
- sourceFile: params.sourceFiles.get(child.path)
248
- }));
249
- continue;
250
- }
251
- if (yield* walkNested(child, params)) exports.push(ast.factory.createExport({ path: toRelativeModulePath({
252
- fromDir: node.path,
253
- filePath: `${child.path}${BARREL_SUFFIX}`
254
- }) }));
255
- }
256
- if (exports.length > 0) {
257
- yield makeBarrel({
258
- dirPath: node.path,
259
- exports,
260
- sourceFiles: params.sourceFiles,
261
- reportedCollisions: params.reportedCollisions
262
- });
263
- return true;
264
- }
265
- return false;
165
+ function walkNested(node, ctx) {
166
+ const results = [...node.children.values()].map((child) => {
167
+ if (!child.children) return {
168
+ barrels: [],
169
+ exports: ctx.leafExports(node.path, child.path)
170
+ };
171
+ const barrels = walkNested(child, ctx);
172
+ return {
173
+ barrels,
174
+ exports: barrels.length > 0 ? [ast.factory.createExport({ path: toRelativeModulePath(node.path, `${child.path}${BARREL_SUFFIX}`) })] : []
175
+ };
176
+ });
177
+ const barrels = results.flatMap((result) => result.barrels);
178
+ const exports = results.flatMap((result) => result.exports);
179
+ return exports.length > 0 ? [...barrels, makeBarrel(node.path, exports, ctx)] : barrels;
266
180
  }
267
181
  /**
268
182
  * Indexes `files` once for the directory rooted at `outputPath`: filters to indexable source
@@ -270,62 +184,62 @@ function* walkNested(node, params) {
270
184
  * derived from the same root rather than re-filtering and re-building per barrel.
271
185
  */
272
186
  function buildBarrelIndex(outputPath, files) {
273
- const outputPrefix = `${toPosixPath(outputPath)}/`;
274
- const sourceFiles = /* @__PURE__ */ new Map();
275
- for (const file of files) {
276
- const normalized = toPosixPath(file.path);
277
- if (!normalized.startsWith(outputPrefix)) continue;
278
- if (isBarrelPath(normalized)) continue;
279
- if (!SOURCE_EXTENSIONS.has(extname(normalized))) continue;
280
- sourceFiles.set(normalized, file);
281
- }
187
+ const tree = {
188
+ path: toPosixPath(outputPath),
189
+ children: /* @__PURE__ */ new Map()
190
+ };
191
+ const sourceFiles = new Map(files.flatMap((file) => {
192
+ const filePath = toPosixPath(file.path);
193
+ return filePath.startsWith(`${tree.path}/`) && !filePath.endsWith(BARREL_SUFFIX) && SOURCE_EXTENSIONS.has(extname(filePath)) ? [[filePath, file]] : [];
194
+ }));
195
+ [...sourceFiles.keys()].map((filePath) => filePath.slice(tree.path.length + 1).split("/").filter(Boolean)).sort(compareSegments).forEach((parts) => {
196
+ parts.reduce((dir, part, i) => {
197
+ const path = `${dir.path}/${part}`;
198
+ const child = dir.children.get(part) ?? (i === parts.length - 1 ? { path } : {
199
+ path,
200
+ children: /* @__PURE__ */ new Map()
201
+ });
202
+ dir.children.set(part, child);
203
+ return child.children ? child : dir;
204
+ }, tree);
205
+ });
282
206
  return {
283
- tree: buildTree(outputPath, [...sourceFiles.keys()]),
207
+ tree,
284
208
  sourceFiles
285
209
  };
286
210
  }
287
211
  /**
288
- * Locates the node for `targetPath` within an index tree, walking down through directory nodes
289
- * only. Returns `undefined` when no file exists at or under `targetPath` (nothing to barrel).
212
+ * Locates the directory node for `targetPath` within an index tree. Returns `undefined` when no
213
+ * file exists under `targetPath` (nothing to barrel).
290
214
  */
291
- function findNode(node, targetPath) {
292
- if (node.path === targetPath) return node;
293
- if (node.isFile || !targetPath.startsWith(`${node.path}/`)) return void 0;
294
- for (const child of node.children) if (!child.isFile && (child.path === targetPath || targetPath.startsWith(`${child.path}/`))) return findNode(child, targetPath);
215
+ function findDirectory(tree, targetPath) {
216
+ if (targetPath === tree.path) return tree;
217
+ if (!targetPath.startsWith(`${tree.path}/`)) return void 0;
218
+ const node = targetPath.slice(tree.path.length + 1).split("/").reduce((current, part) => current?.children?.get(part), tree);
219
+ return node?.children ? node : void 0;
295
220
  }
296
221
  /**
297
- * Yields barrel `ast.FileNode`s for `targetPath` (or the index root), derived from a shared index.
222
+ * Returns the barrel `ast.FileNode`s for `targetPath` (or the index root), derived from a shared index.
298
223
  * Locating the subtree is a bounded walk down from the root, so deriving many barrels (one per
299
224
  * plugin, plus the root) from one index avoids re-scanning the full file set for each.
300
225
  *
301
226
  * @example
302
227
  * ```ts
303
228
  * const index = buildBarrelIndex(outputPath, files)
304
- * for (const file of getBarrelFiles({ index, targetPath, barrelType })) {
305
- * upsertFile(file)
306
- * }
229
+ * getBarrelFiles({ index, targetPath, barrelType }).forEach((file) => upsertFile(file))
307
230
  * ```
308
231
  */
309
- function* getBarrelFiles({ index, targetPath, barrelType, nested = false, recursive = false, reportedCollisions = /* @__PURE__ */ new Set() }) {
310
- const node = targetPath ? findNode(index.tree, toPosixPath(targetPath)) : index.tree;
311
- if (!node) return;
232
+ function getBarrelFiles({ index, targetPath, barrelType, nested = false, recursive = false, reportedCollisions = /* @__PURE__ */ new Set() }) {
233
+ const node = targetPath ? findDirectory(index.tree, toPosixPath(targetPath)) : index.tree;
234
+ if (!node) return [];
312
235
  const strategy = barrelType === "named" ? namedStrategy : allStrategy;
313
- if (nested) {
314
- yield* walkNested(node, {
315
- sourceFiles: index.sourceFiles,
316
- strategy,
317
- reportedCollisions
318
- });
319
- return;
320
- }
321
- yield* walkAllOrNamed(node, {
236
+ const ctx = {
322
237
  sourceFiles: index.sourceFiles,
323
- strategy,
324
- reportedCollisions
325
- }, {
326
- isRoot: true,
327
- recursive
328
- });
238
+ reportedCollisions,
239
+ recursive,
240
+ leafExports: (dirPath, leafPath) => strategy(toRelativeModulePath(dirPath, leafPath), index.sourceFiles.get(leafPath))
241
+ };
242
+ return nested ? walkNested(node, ctx) : walkFlat(node, ctx).barrels;
329
243
  }
330
244
  //#endregion
331
245
  //#region src/plugin.ts
@@ -365,6 +279,7 @@ const pluginBarrelName = "plugin-barrel";
365
279
  const states = /* @__PURE__ */ new WeakMap();
366
280
  function getState(config) {
367
281
  const state = states.get(config) ?? {
282
+ excludedPlugins: /* @__PURE__ */ new Set(),
368
283
  excludedTargets: [],
369
284
  pendingBarrels: []
370
285
  };
@@ -405,19 +320,25 @@ function getState(config) {
405
320
  * ```
406
321
  */
407
322
  const pluginBarrel = definePlugin({
408
- meta: { name: pluginBarrelName },
323
+ meta: {
324
+ name: pluginBarrelName,
325
+ version,
326
+ compatibility
327
+ },
409
328
  enforce: "post",
410
329
  hooks: {
411
330
  "kubb:plugin:end"({ plugin, config }) {
412
331
  if (plugin.name === "plugin-barrel") return;
413
- const { excludedTargets, pendingBarrels } = getState(config);
332
+ const { excludedPlugins, excludedTargets, pendingBarrels } = getState(config);
414
333
  const barrelConfig = plugin.options.output?.barrel ?? config.output.barrel ?? false;
415
334
  const base = path.resolve(config.root, config.output.path);
416
- const target = path.resolve(base, plugin.options.output.path);
335
+ const target = path.resolve(base, plugin.options.output.path ?? ".");
417
336
  if (barrelConfig === false) {
418
- excludedTargets.push(target);
337
+ excludedPlugins.add(plugin.name);
338
+ if (target !== base && isPathInside(target, base)) excludedTargets.push(target);
419
339
  return;
420
340
  }
341
+ if (target === base) return;
421
342
  if (plugin.options.output.mode === "file") return;
422
343
  if (!isPathInside(target, base)) throw new Diagnostics.Error({
423
344
  code: Diagnostics.code.pathTraversal,
@@ -434,32 +355,43 @@ const pluginBarrel = definePlugin({
434
355
  });
435
356
  },
436
357
  "kubb:plugins:end"({ files, config, upsertFile }) {
437
- const { excludedTargets, pendingBarrels } = getState(config);
358
+ const { excludedPlugins, excludedTargets, pendingBarrels } = getState(config);
438
359
  const rootBarrelConfig = config.output.barrel ?? false;
439
360
  const outputPath = path.resolve(config.root, config.output.path);
440
- const relevantFiles = files.filter((file) => !excludedTargets.some((target) => isPathInside(file.path, target)));
361
+ const relevantFiles = files.filter((file) => {
362
+ const owner = file.meta?.pluginName;
363
+ if (owner) return !excludedPlugins.has(owner);
364
+ return !excludedTargets.some((target) => isPathInside(file.path, target));
365
+ });
366
+ excludedPlugins.clear();
441
367
  excludedTargets.length = 0;
442
368
  const index = buildBarrelIndex(outputPath, relevantFiles);
443
369
  const reportedCollisions = /* @__PURE__ */ new Set();
444
- for (const { plugin, target, barrelType, nested } of pendingBarrels) for (const file of getBarrelFiles({
445
- index,
446
- targetPath: target,
447
- barrelType,
448
- nested,
449
- recursive: true,
450
- reportedCollisions
451
- })) upsertFile(withBarrelBannerFooter({
452
- file,
453
- plugin,
454
- config
455
- }));
370
+ pendingBarrels.forEach(({ plugin, target, barrelType, nested }) => {
371
+ getBarrelFiles({
372
+ index,
373
+ targetPath: target,
374
+ barrelType,
375
+ nested,
376
+ recursive: true,
377
+ reportedCollisions
378
+ }).forEach((file) => {
379
+ upsertFile(withBarrelBannerFooter({
380
+ file,
381
+ plugin,
382
+ config
383
+ }));
384
+ });
385
+ });
456
386
  pendingBarrels.length = 0;
457
387
  if (rootBarrelConfig === false) return;
458
- for (const file of getBarrelFiles({
388
+ getBarrelFiles({
459
389
  index,
460
390
  barrelType: rootBarrelConfig.type,
461
391
  reportedCollisions
462
- })) upsertFile(file);
392
+ }).forEach((file) => {
393
+ upsertFile(file);
394
+ });
463
395
  }
464
396
  }
465
397
  });