Skip to content

feat(eslint-rules): add prefer-direct-reexport eslint rule - #36581

Merged
Victor Genaev (mainframev) merged 9 commits into
masterfrom
mainframev/prefer-direct-reexport-rule
Aug 19, 2026
Merged

feat(eslint-rules): add prefer-direct-reexport eslint rule#36581
Victor Genaev (mainframev) merged 9 commits into
masterfrom
mainframev/prefer-direct-reexport-rule

Conversation

@mainframev

@mainframev Victor Genaev (mainframev) commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Adds a new workspace ESLint rule @nx/workspace-prefer-direct-reexport.

It flags exported bindings that are just an alias of something imported from another module, and points at the direct export … from form instead:

// flagged
import { renderToolbar_unstable } from '@fluentui/react-toolbar';
export const renderToolbar = renderToolbar_unstable;

// preferred
export { renderToolbar_unstable as renderToolbar } from '@fluentui/react-toolbar';

What detects

Aliasing an import through a local binding

// ❌ reported
import { renderBase } from 'pkg';
export const render = renderBase;

// ✅ preferred
export { renderBase as render } from 'pkg';

** Default import re-exported under a name**

// ❌ reported
import renderBase from 'pkg';
export const render = renderBase;

// ✅ preferred
export { default as render } from 'pkg';

Named import re-exported as default

// ❌ reported
import { renderBase } from 'pkg';
export default renderBase;

// ✅ preferred
export { renderBase as default } from 'pkg';

Namespace import re-exported under a name

// ❌ reported
import * as local from 'pkg';
export { local as publicName };

// ✅ preferred
export * as publicName from 'pkg';

Alias chains through intermediate bindings

// ❌ reported
import { renderBase } from 'pkg';
const localRender = renderBase;
export const render = localRender;

// ✅ preferred
export { renderBase as render } from 'pkg';

Identity function wrappers

// ❌ reported — forwards its arguments unchanged
import { renderBase } from 'pkg';
export const render = props => renderBase(props);

// ❌ reported
export function render(props, options) {
  return renderBase(props, options);
}

// ✅ preferred
export { renderBase as render } from 'pkg';

Type-only aliases

// ❌ reported
import type { BaseProps } from 'pkg';
export type Props = BaseProps;

// ✅ preferred
export type { BaseProps as Props } from 'pkg';

What it leaves alone

Anything that changes the public shape, is reassignable, or has no export … from equivalent:

// the annotation narrows the public signature — not an identity re-export
export const render = (value: unknown) => renderBase(value);

//  the type annotation is the public contract
type PublicSignature = typeof renderBase;
export const render: PublicSignature = renderBase;

// reassignable, so it is not a re-export
export let render = renderBase;

// wrapping changes runtime behaviour
export const render = async props => renderBase(props);
export function* render(props) {
  return renderBase(props);
}

// composed or instantiated types are new types
export type Props = BaseProps & OtherProps;
export type Props = BaseProps<any>;
export type Props<T> = BaseProps;
export interface Props extends BaseProps {}

// a member of a namespace, not the namespace itself
import * as upstream from 'pkg';
export const render = upstream.renderBase;

// no `export … from` form exists for a namespace as default
import * as local from 'pkg';
export default local;

Perf check

Measured on CI in a fork with two PRs differing only by this rule (baseline vs with rule). Full workspace lint, 247 projects, Nx cache disabled, ubuntu-latest, 3 runs each.

Duration
Without rule 1148s
With rule 1149s
Delta +1s (+0.09%)

Per-rule cost from ESLint's own profiler (TIMING=100) on react-components/react-button/library:

Rule Time
@typescript-eslint/no-deprecated 570.837 ms
@nx/workspace-base-hook-no-forbidden-runtime 25.372 ms
@nx/workspace-prefer-direct-reexport 0.396 ms

@github-actions

Copy link
Copy Markdown

📊 Bundle size report

✅ No changes found

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

Comment thread packages/eslint-plugin/src/internal.js Outdated
Comment thread packages/eslint-plugin/src/internal.js

@Hotell Martin Hochel (Hotell) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great addition to our harness, left some comments. ty

Comment thread tools/eslint-rules/rules/prefer-direct-reexport.spec.ts Outdated
Comment thread tools/eslint-rules/rules/prefer-direct-reexport.spec.ts
Comment thread tools/eslint-rules/rules/prefer-direct-reexport.spec.ts
@Hotell Martin Hochel (Hotell) changed the title feat(tools): add prefer-direct-reexport eslint rule feat(eslint-rules): add prefer-direct-reexport eslint rule Aug 17, 2026

@Hotell Martin Hochel (Hotell) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments, nothing blocking

Comment thread tools/eslint-rules/rules/prefer-direct-reexport.spec.ts
Comment thread tools/eslint-rules/rules/prefer-direct-reexport.spec.ts
@mainframev
Victor Genaev (mainframev) force-pushed the mainframev/prefer-direct-reexport-rule branch from da68154 to 8fed98c Compare August 19, 2026 11:03
@mainframev
Victor Genaev (mainframev) merged commit 3685a68 into master Aug 19, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants