Skip to content

Commit 513a65d

Browse files
committed
chore: improve doc
1 parent a142c72 commit 513a65d

6 files changed

Lines changed: 54 additions & 13 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
<h2 align="center">barrel-reaper</h2>
66
<p align="center"><em>Barrel files sunt diabolus. Amen.</em><br>Rewrites barrel imports as direct imports at the source. </p>
7+
<p align="center"><sub>Parses with <a href="https://oxc.rs">oxc</a>, walks files in parallel with <a href="https://github.com/rayon-rs/rayon">rayon</a>.</sub></p>
78

89

910
---
@@ -33,7 +34,7 @@ Options:
3334
## Examples
3435

3536
```sh
36-
# dry run show what would change
37+
# dry run: show what would change
3738
barrel-reaper -b src/lib/index.ts -a @lib -d
3839

3940
# rewrite in place, scoped to a subtree
@@ -43,6 +44,27 @@ barrel-reaper -b src/lib/index.ts -a @lib -g 'src/app/**'
4344
barrel-reaper -b src/lib/index.ts
4445
```
4546

47+
## Monorepos & path aliases
48+
49+
Resolution is delegated to the nearest `tsconfig.json` discovered per-file,
50+
falling back to `node_modules` for bare specifiers outside of `paths`.
51+
**Install your dependencies first** (`pnpm install`, `yarn`, etc.) so
52+
workspace packages are symlinked under `node_modules`. Without this, bare
53+
aliases like `@scope/pkg` can't be resolved and those imports are silently
54+
treated as non-barrel.
55+
56+
If reaper finds fewer files than expected, run with `--debug`:
57+
58+
```sh
59+
barrel-reaper -b packages/components/index.ts -g 'apps/account/**' \
60+
-a @scope/components -d --debug
61+
```
62+
63+
For every file that *mentions* the barrel but whose imports didn't classify,
64+
it prints each specifier's resolver outcome on stderr: enough to pinpoint
65+
whether it's a missing `paths` entry, a sub-path import, or an uninstalled
66+
workspace package.
67+
4668
## Library
4769

4870
```rust

src/exports.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ fn collect_into(
6767
Statement::ExportNamedDeclaration(decl) => {
6868
handle_export_named(decl, file, barrel_dir, alias, resolver, exports);
6969
}
70+
// Only the barrel's own `export default` becomes the published
71+
// default. Defaults re-exported from deeper files arrive as
72+
// named specifiers and are handled by `handle_export_named`.
7073
Statement::ExportDefaultDeclaration(_) if file == barrel => {
7174
exports.insert(
7275
"default".to_string(),
@@ -96,6 +99,7 @@ fn handle_export_all(
9699
let resolved = resolver.resolve(from_module, file);
97100

98101
match &decl.exported {
102+
// `export * as ns from './x'`: publishes `ns` as a single namespace.
99103
Some(name_node) => {
100104
exports.insert(
101105
name_node.name().to_string(),
@@ -106,6 +110,7 @@ fn handle_export_all(
106110
},
107111
);
108112
}
113+
// `export * from './x'`: recurse and hoist every named export.
109114
None => {
110115
if let Some(target) = resolved {
111116
collect_into(&target, barrel, alias, resolver, exports, visited);

src/imports.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use rayon::prelude::*;
1313
use crate::resolver::ModuleResolver;
1414
use crate::{Context, SymbolKind};
1515

16+
/// Skipped even when not in `.gitignore` (common build outputs that
17+
/// projects routinely forget to ignore).
1618
const DEFAULT_IGNORED_DIRS: &[&str] = &[
1719
"node_modules",
1820
"target",
@@ -55,11 +57,10 @@ pub fn find_barrel_imports(ctx: &Context, resolver: &ModuleResolver) -> Vec<Barr
5557
})
5658
}
5759

58-
/// Shared walker + discover pipeline. Canonicalizes once, streams entries
59-
/// through rayon via `par_bridge`, reads each file a single time, pre-filters
60-
/// by needle, parses, extracts barrel-import statements, then hands
61-
/// `(path, source, statements)` to `process`. Files with no barrel imports
62-
/// are dropped before `process` runs.
60+
/// Discovery pipeline: walk, fast-string pre-filter, parse, extract barrel
61+
/// import statements, then hand `(path, source, statements)` to `process`.
62+
/// Each file is read exactly once; files with no barrel imports are dropped
63+
/// before `process` runs.
6364
pub(crate) fn walk_barrel_candidates<T, F>(
6465
ctx: &Context,
6566
resolver: &ModuleResolver,
@@ -72,9 +73,8 @@ where
7273
let Ok(barrel_path) = fs::canonicalize(&ctx.barrel_file) else {
7374
return Vec::new();
7475
};
75-
// Canonicalize root once so walker entries are absolute — oxc_resolver's
76-
// per-file tsconfig walk-up needs an absolute anchor, and this avoids a
77-
// syscall per candidate.
76+
// oxc_resolver's per-file tsconfig walk-up needs absolute paths.
77+
// Canonicalizing the root once avoids a syscall per candidate.
7878
let Ok(root) = fs::canonicalize(&ctx.root_dir) else {
7979
return Vec::new();
8080
};
@@ -137,6 +137,9 @@ where
137137
results
138138
}
139139

140+
/// Short strings any barrel-importing file must contain. The alias catches
141+
/// `@lib`-style imports; the parent dir name catches relative imports like
142+
/// `../lib` (index files are referenced by their folder).
140143
fn build_needles(ctx: &Context) -> Vec<String> {
141144
let mut needles = Vec::with_capacity(2);
142145
if let Some(alias) = &ctx.barrel_alias {
@@ -247,6 +250,8 @@ fn extract_specifiers(decl: &ImportDeclaration) -> Vec<BarrelImport> {
247250
type_import: stmt_type_only || s.import_kind.is_type(),
248251
})
249252
}
253+
// `import * as X from barrel` pulls in the whole module;
254+
// nothing to split, so we leave it untouched.
250255
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => None,
251256
})
252257
.collect()

src/path.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub fn resolve_export_path(from_module: &str, alias: Option<&str>) -> String {
1414
}
1515
}
1616

17+
/// Relative import string between two files. Always prefixed with `./` when
18+
/// not climbing with `../`, since bare paths would parse as bare-package
19+
/// specifiers.
1720
pub fn get_import_path(from_file: &Path, to_file: &Path) -> String {
1821
let from_dir = from_file.parent().unwrap_or(Path::new(""));
1922
let rel = pathdiff::diff_paths(to_file, from_dir).unwrap_or_else(|| to_file.to_path_buf());

src/reaper.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ pub fn rewrite(
2626
.iter()
2727
.any(|imp| resolves(ctx, exports, &imp.import_name));
2828

29-
// If no specifier in this statement resolves, leaving it intact
30-
// keeps the consumer compilable (it would fail the same way it did
31-
// before reaper ran) — dropping it gains nothing. Still flag each
32-
// name so the CLI can surface the mismatch.
29+
// Leave the statement intact when nothing resolves: the consumer
30+
// stays as-broken-as-before, and dropping it would only mask the
31+
// mismatch. Names still flow into `unresolved` for the CLI.
3332
if !any_resolvable {
3433
unresolved.extend(stmt.imports.iter().map(|i| i.import_name.clone()));
3534
continue;
@@ -102,6 +101,8 @@ fn format_import(
102101
}
103102
}
104103

104+
/// Swallow the trailing newline so removing the statement doesn't leave a
105+
/// blank line behind.
105106
fn expand_to_line_end(source: &str, span: Span) -> Span {
106107
let end = span.end as usize;
107108
let new_end = source[end..]
@@ -110,6 +111,7 @@ fn expand_to_line_end(source: &str, span: Span) -> Span {
110111
Span::new(span.start, new_end as u32)
111112
}
112113

114+
/// Assumes `spans` is sorted ascending and non-overlapping.
113115
fn remove_spans(source: &str, spans: &[Span]) -> String {
114116
let mut out = String::with_capacity(source.len());
115117
let mut cursor = 0;

src/resolver.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ impl ModuleResolver {
1212
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
1313
.map(String::from)
1414
.to_vec(),
15+
// Auto walks up from each resolved file to find the nearest
16+
// tsconfig. Required for monorepos where `paths` differ per package.
1517
tsconfig: Some(TsconfigDiscovery::Auto),
1618
..ResolveOptions::default()
1719
};
@@ -20,6 +22,8 @@ impl ModuleResolver {
2022
}
2123
}
2224

25+
/// Must use `resolve_file` (not the dir-form API): `TsconfigDiscovery::Auto`
26+
/// is silently ignored by the dir form.
2327
pub fn resolve(&self, specifier: &str, from_file: &Path) -> Option<PathBuf> {
2428
self.inner
2529
.resolve_file(from_file, specifier)

0 commit comments

Comments
 (0)