Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Documentation lives in two places:
- **Testing**: Vitest (colocated tests)
- **Components**: `packages/core/`
- **Storybook**: `apps/storybook/`
- **Imports inside a package**: import the defining module (`'../List/List'`), never a folder's index barrel (`'../List'`); public consumers import `@astryxdesign/core`. `@astryx/no-internal-barrel-import` enforces it.

## JSDoc Conventions

Expand Down
45 changes: 45 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {defineConfig} from 'eslint/config';
import {includeIgnoreFile} from '@eslint/compat';
import path from 'node:path';
import {readFileSync} from 'node:fs';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintReact from '@eslint-react/eslint-plugin';
Expand Down Expand Up @@ -31,6 +32,29 @@
: astryxPlugin.configs.recommended;
const reactSeverity = isStrictMode ? 'error' : 'warn';

// Barrel imports that existed when `@astryx/no-internal-barrel-import` arrived,
// by repo-relative file. The list only shrinks.
const internalBarrelBaseline = JSON.parse(
readFileSync(
new URL(

Check failure on line 39 in eslint.config.js

View workflow job for this annotation

GitHub Actions / lint

'URL' is not defined

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.

URL is not declared for this file (only /* global process */ at line 13), so pnpm lint reports 'URL' is not defined (no-undef) here in strict and local mode; the Lint check for this head fails on it. Reading the file the way the .gitignore imports do avoids the global: readFileSync(path.join(import.meta.dirname, 'internal/eslint-plugin-astryx/no-internal-barrel-import.baseline.json'), 'utf8').

'./internal/eslint-plugin-astryx/no-internal-barrel-import.baseline.json',
import.meta.url,
),
'utf8',
),
);
// Tests, stories and test helpers never ship, so they may import barrels.
const INTERNAL_BARREL_TEST_FILES = [
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/*.testing.*',
'**/*.render-testing.*',
'**/__tests__/**',
'**/__fixtures__/**',
'**/dist/**',
];

// The internal plugin is plain untyped JS (kept out of the lint/type surface),
// so its inferred shape doesn't satisfy ESLint's strict `Plugin` type. One
// localized assertion here keeps every `plugins` entry below type-checked.
Expand Down Expand Up @@ -863,6 +887,27 @@
//
// Remove an entry here as its backlog closes; the file is clean when the
// block is empty.
// Inside a package's own shipped source, import the module that defines a

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.

This block lands between richtext's backlog comment (lines 878-892) and the richtext block at line 914, so that comment now reads as if it describes the barrel rule. Moving this block and its comment above line 878 keeps richtext's block last and its comment attached.

// name, not a folder's index (a barrel): a lazily imported module that goes
// through a barrel pulls every sibling export into its graph, which broke
// the single-file bundle. Public consumers keep importing from the package
// entry. The barrel imports that predate the rule are listed in the
// baseline and stay allowed until a codemod removes them; anything new is
// reported (an error in strict mode, so a pull request that adds one fails
// lint, while main stays green). Type-only imports are erased and allowed.
{
files: ['packages/*/src/**/*.{ts,tsx,mts,cts,js,jsx,mjs}'],
ignores: INTERNAL_BARREL_TEST_FILES,

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.

This block also covers package entries and component barrels. Adding export * from './Code'; to packages/core/src/index.ts, the step CONTRIBUTING.md prescribes for a new component, fails strict lint ('./Code' resolves to a folder's index). Following the message there (export * from './Dialog/Dialog') would publish exports the barrel leaves out. Adding '**/index.*' to these ignores keeps the lazy-graph protection, because an ordinary module's barrel import is still reported, and lets the 13 index-file keys leave the baseline.

plugins: {
'@astryx': astryxEslintPlugin,
},
rules: {
'@astryx/no-internal-barrel-import': [
reactSeverity,
{baseline: internalBarrelBaseline},
],
},
},
{
files: ['packages/richtext/src/**/*.{ts,tsx}'],
rules: {
Expand Down
19 changes: 19 additions & 0 deletions internal/eslint-plugin-astryx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,25 @@ function useSortedNames(names: string[]) {
}
```

### `@astryx/no-internal-barrel-import`

Reports a relative import, inside a package's own shipped source, that
resolves to a folder's index file (a barrel) instead of the module that defines
what it imports: `import {List} from '../List'` rather than
`import {List} from '../List/List'`. A barrel pulls every sibling export into
the importer's module graph, so a lazily imported module that goes through one
drags unrelated code, and in a single-file bundle import cycles, along with it.
It checks `import`, `export … from` and literal `import()`; type-only imports
and exports are erased by the compiler and allowed. Public consumers keep
importing from the package entry (`@astryxdesign/core`), which this rule never
reads.

The repo config turns it on for `packages/*/src`, outside tests, stories and
test helpers, at the strict-mode severity (an error in CI). Barrel imports that
existed when the rule arrived are listed by file in
`no-internal-barrel-import.baseline.json` and stay allowed until a codemod
removes them; remove an entry when you fix its import, and never add one.

### `@astryx/no-core-internal-import`

Reports imports of Core's internal subpaths (`@astryxdesign/core/internal` and
Expand Down
3 changes: 3 additions & 0 deletions internal/eslint-plugin-astryx/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* - no-unstable-merged-refs: Flags render-time mergeRefs callbacks and unstable callback inputs to useMergedRefs
* - no-light-dark-outside-theme: Flags CSS light-dark() in component source (a light/dark decision belongs to the theme layer, where a token pair reaches both schemes in every theme)
* - no-core-internal-import: Keeps Core's internal subpaths (@astryxdesign/core/internal/*) inside Core and Lab
* - no-internal-barrel-import: Flags a package-internal relative import that resolves to a folder's index (a barrel) instead of the defining module
* - no-raw-color: Flags a raw colour value (hex, rgb(), hsl(), oklch(), …) anywhere in component source, including inside light-dark()/color-mix(), behind a const, in a template literal, or as a var() fallback — the shapes no-hardcoded-styles cannot see
*
* Philosophy: Strict for agents (CI), lenient for humans (local dev)
Expand Down Expand Up @@ -54,6 +55,7 @@ import noUnstableMergedRefsRule from './no-unstable-merged-refs.js';
import copyrightHeaderRule from './copyright-header.js';
import noRawConsoleCliRule from './no-raw-console-cli.js';
import noCoreInternalImportRule from './no-core-internal-import.js';
import noInternalBarrelImportRule from './no-internal-barrel-import.js';
import requireBasePropsRule from './require-base-props.js';
import requireRefPropRule from './require-ref-prop.js';
import requireBasePropsPassthroughRule from './require-baseprops-passthrough.js';
Expand Down Expand Up @@ -360,6 +362,7 @@ const plugin = {
'copyright-header': copyrightHeaderRule,
'no-raw-console-cli': noRawConsoleCliRule,
'no-core-internal-import': noCoreInternalImportRule,
'no-internal-barrel-import': noInternalBarrelImportRule,
'no-hardcoded-i18n-string': noHardcodedI18nStringRule,
'i18n-key-format': i18nKeyFormatRule,
'require-table-section': requireTableSectionRule,
Expand Down
Loading
Loading