Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions src/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use oxc_ast::ast::*;
use oxc_parser::Parser;
use oxc_span::SourceType;

use crate::path::{barrel_export_path, resolve_export_path};
use crate::path::{barrel_export_path, resolved_export_path};
use crate::resolver::ModuleResolver;
use crate::{Context, SymbolKind};

Expand Down Expand Up @@ -62,7 +62,9 @@ fn collect_into(
for stmt in &parsed.program.body {
match stmt {
Statement::ExportAllDeclaration(decl) => {
handle_export_all(decl, file, barrel, alias, resolver, exports, visited);
handle_export_all(
decl, file, barrel, barrel_dir, alias, resolver, exports, visited,
);
}
Statement::ExportNamedDeclaration(decl) => {
handle_export_named(decl, file, barrel_dir, alias, resolver, exports);
Expand Down Expand Up @@ -90,6 +92,7 @@ fn handle_export_all(
decl: &ExportAllDeclaration,
file: &Path,
barrel: &Path,
barrel_dir: &Path,
alias: Option<&str>,
resolver: &ModuleResolver,
exports: &mut HashMap<String, BarrelExport>,
Expand All @@ -104,7 +107,12 @@ fn handle_export_all(
exports.insert(
name_node.name().to_string(),
BarrelExport {
source_path: resolve_export_path(from_module, alias),
source_path: resolved_export_path(
resolved.as_deref(),
from_module,
barrel_dir,
alias,
),
kind: SymbolKind::Named,
source_file_path: resolved,
},
Expand All @@ -130,23 +138,8 @@ fn handle_export_named(
if let Some(source) = &decl.source {
let from_module = source.value.as_str();
let resolved = resolver.resolve(from_module, file);
let source_path = resolve_export_path(from_module, alias);

for spec in &decl.specifiers {
let kind = if spec.local.name() == "default" {
SymbolKind::Default
} else {
SymbolKind::Named
};
exports.insert(
spec.exported.name().to_string(),
BarrelExport {
source_path: source_path.clone(),
kind,
source_file_path: resolved.clone(),
},
);
}
let source_path = resolved_export_path(resolved.as_deref(), from_module, barrel_dir, alias);
insert_reexports(&decl.specifiers, &source_path, resolved.as_deref(), exports);
} else if let Some(declaration) = &decl.declaration {
let source_path = barrel_export_path(file, barrel_dir, alias);
for name in declaration_names(declaration) {
Expand All @@ -159,6 +152,34 @@ fn handle_export_named(
},
);
}
} else if !decl.specifiers.is_empty() {
// `export { foo };` — republishing a local/imported binding. Point
// at the current file and let TS follow any further re-export chain.
let source_path = barrel_export_path(file, barrel_dir, alias);
insert_reexports(&decl.specifiers, &source_path, Some(file), exports);
}
}

fn insert_reexports(
specifiers: &[ExportSpecifier<'_>],
source_path: &str,
source_file_path: Option<&Path>,
exports: &mut HashMap<String, BarrelExport>,
) {
for spec in specifiers {
let kind = if spec.local.name() == "default" {
SymbolKind::Default
} else {
SymbolKind::Named
};
exports.insert(
spec.exported.name().to_string(),
BarrelExport {
source_path: source_path.to_string(),
kind,
source_file_path: source_file_path.map(Path::to_path_buf),
},
);
}
}

Expand Down
25 changes: 24 additions & 1 deletion src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,36 @@ pub fn barrel_export_path(file: &Path, barrel_dir: &Path, alias: Option<&str>) -
format!("{prefix}/{}", without_ext.display())
}

pub fn resolve_export_path(from_module: &str, alias: Option<&str>) -> String {
/// Picks the right `source_path` for a re-export. When the target resolves
/// inside `barrel_dir`, render it as a barrel-space path (alias or `./`);
/// otherwise fall back to aliasing the literal specifier — a later
/// consumer-relative rewrite in `reaper::format_import` will rescue
/// out-of-barrel cases when we have a resolved target.
pub fn resolved_export_path(
resolved: Option<&Path>,
literal: &str,
barrel_dir: &Path,
alias: Option<&str>,
) -> String {
match resolved {
Some(target) if target.starts_with(barrel_dir) => {
barrel_export_path(target, barrel_dir, alias)
}
_ => aliased_literal(literal, alias),
}
}

fn aliased_literal(from_module: &str, alias: Option<&str>) -> String {
match (alias, from_module.strip_prefix("./")) {
(Some(alias), Some(rest)) => format!("{alias}/{rest}"),
_ => from_module.to_string(),
}
}

pub fn is_relative_specifier(spec: &str) -> bool {
spec.starts_with("./") || spec.starts_with("../")
}

/// Relative import string between two files. Always prefixed with `./` when
/// not climbing with `../`, since bare paths would parse as bare-package
/// specifiers.
Expand Down
23 changes: 17 additions & 6 deletions src/reaper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use oxc_span::Span;

use crate::exports::BarrelExport;
use crate::imports::{BarrelImport, BarrelImportStatement};
use crate::path::get_import_path;
use crate::path::{get_import_path, is_relative_specifier};
use crate::{Context, ReapedFile, SymbolKind};

pub fn rewrite(
Expand Down Expand Up @@ -64,10 +64,16 @@ pub fn rewrite(
}
}

/// Relative mode needs an absolute target to render a path relative to the
/// consumer. Alias mode prefixes the stored `source_path` and is unaffected.
/// With a resolved target we can always render a path (alias path if the
/// target is inside the barrel, otherwise consumer-relative). Without one,
/// alias mode can still emit the stored `source_path` — but only if it's a
/// bare-package-style specifier; a relative literal would be wrong to paste
/// into the consumer.
fn can_rewrite(ctx: &Context, export: &BarrelExport) -> bool {
ctx.barrel_alias.is_some() || export.source_file_path.is_some()
if export.source_file_path.is_some() {
return true;
}
ctx.barrel_alias.is_some() && !is_relative_specifier(&export.source_path)
}

fn resolves(ctx: &Context, exports: &HashMap<String, BarrelExport>, name: &str) -> bool {
Expand All @@ -80,8 +86,13 @@ fn format_import(
from_file: &Path,
ctx: &Context,
) -> String {
let source_path = match (&ctx.barrel_alias, &export.source_file_path) {
(None, Some(target)) => get_import_path(from_file, target),
// Use the resolved target (consumer-relative) whenever the stored
// `source_path` isn't a valid specifier to paste into the consumer: in
// relative mode always, in alias mode when the target lives outside the
// barrel's alias space (`source_path` kept its relative literal).
let use_target = ctx.barrel_alias.is_none() || is_relative_specifier(&export.source_path);
let source_path = match &export.source_file_path {
Some(target) if use_target => get_import_path(from_file, target),
_ => export.source_path.clone(),
};
let type_prefix = if imp.type_import { "type " } else { "" };
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/nested-reexports/barrel/feature/extras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ex1 = 1;
export const ex2 = 2;
2 changes: 2 additions & 0 deletions tests/fixtures/nested-reexports/barrel/feature/impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const foo = 1;
export type FooType = string;
5 changes: 5 additions & 0 deletions tests/fixtures/nested-reexports/barrel/feature/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { foo } from './impl';
export type { FooType } from './impl';
export * as extras from './extras';
export * from './wrapper';
export { nestedExternal } from '../../shared/utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const wrapped = 42;
2 changes: 2 additions & 0 deletions tests/fixtures/nested-reexports/barrel/feature/wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { wrapped } from './wrapper-impl';
export { wrapped };
2 changes: 2 additions & 0 deletions tests/fixtures/nested-reexports/barrel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './feature';
export { external } from '../shared/utils';
2 changes: 2 additions & 0 deletions tests/fixtures/nested-reexports/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const external = 'root';
export const nestedExternal = 'nested';
8 changes: 8 additions & 0 deletions tests/fixtures/nested-reexports/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@barrel": ["./barrel/index"],
"@barrel/*": ["./barrel/*"]
}
}
}
14 changes: 14 additions & 0 deletions tests/fixtures/nested-reexports/via-alias/consumer.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { foo } from '@barrel/feature/impl';
import { extras } from '@barrel/feature/extras';
import { wrapped } from '@barrel/feature/wrapper';
import { external } from '../shared/utils';
import { nestedExternal } from '../shared/utils';
import type { FooType } from '@barrel/feature/impl';

void foo;
void extras;
void wrapped;
void external;
void nestedExternal;
const fx: FooType = '';
void fx;
10 changes: 10 additions & 0 deletions tests/fixtures/nested-reexports/via-alias/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { foo, extras, wrapped, external, nestedExternal } from '@barrel';
import type { FooType } from '@barrel';

void foo;
void extras;
void wrapped;
void external;
void nestedExternal;
const fx: FooType = '';
void fx;
14 changes: 14 additions & 0 deletions tests/fixtures/nested-reexports/via-relative/consumer.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { foo } from '../barrel/feature/impl';
import { extras } from '../barrel/feature/extras';
import { wrapped } from '../barrel/feature/wrapper';
import { external } from '../shared/utils';
import { nestedExternal } from '../shared/utils';
import type { FooType } from '../barrel/feature/impl';

void foo;
void extras;
void wrapped;
void external;
void nestedExternal;
const fx: FooType = '';
void fx;
10 changes: 10 additions & 0 deletions tests/fixtures/nested-reexports/via-relative/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { foo, extras, wrapped, external, nestedExternal } from '../barrel';
import type { FooType } from '../barrel';

void foo;
void extras;
void wrapped;
void external;
void nestedExternal;
const fx: FooType = '';
void fx;
29 changes: 29 additions & 0 deletions tests/reaper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,35 @@ fn reaps_through_chained_barrels() {
);
}

// ---------- fixture: nested-reexports (named/namespace + specifier-only +
// out-of-barrel re-exports, some reached via `export *`) ----------

const NESTED_BARREL: &str = "tests/fixtures/nested-reexports/barrel/index.ts";

#[test]
fn reaps_nested_reexports_via_alias() {
assert_reap_matches(
&ctx(
NESTED_BARREL,
Some("@barrel"),
"fixtures/nested-reexports/via-alias/**",
),
"tests/fixtures/nested-reexports/via-alias/consumer.expected",
);
}

#[test]
fn reaps_nested_reexports_via_relative() {
assert_reap_matches(
&ctx(
NESTED_BARREL,
None,
"fixtures/nested-reexports/via-relative/**",
),
"tests/fixtures/nested-reexports/via-relative/consumer.expected",
);
}

// ---------- fixture: mixed (default + named + renamed + type) ----------

#[test]
Expand Down
Loading