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
5 changes: 5 additions & 0 deletions .changeset/remove-forward-export-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ankhorage/devtools': patch
---

Remove the organization-wide `ankhorage/no-forward-exports` ESLint policy and its package-entrypoint resolver. Forward-export and module-ownership decisions are no longer enforced globally by Devtools.
10 changes: 2 additions & 8 deletions src/tools/eslint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
*
* Every profile includes the shared TypeScript, import, unused-import, Prettier, security, and
* quality rules. The common quality limits are 50 effective lines per function, 300 effective
* lines per file, and modified cyclomatic complexity 15. Forward exports are forbidden outside
* declared package entrypoints and explicit `index.*` barrels so implementation files export only
* symbols they own. React adds React and Hooks correctness rules; React Native composes the React
* profile and adds focused React Native rules.
* lines per file, and modified cyclomatic complexity 15. React adds React and Hooks correctness
* rules; React Native composes the React profile and adds focused React Native rules.
*
* Repository-specific behavior stays additive: `additionalIgnores`, `restrictedImports`, and
* `overrides` extend the central policy instead of replacing it. Narrow local overrides remain the
Expand All @@ -34,8 +32,6 @@ import simpleImportSort from 'eslint-plugin-simple-import-sort';
import unusedImports from 'eslint-plugin-unused-imports';
import tseslint from 'typescript-eslint';

import { createModuleOwnershipConfig } from './moduleOwnership.js';
import { resolvePackageEntrypointFiles } from './packageEntrypoints.js';
import { resolveEslintProfile } from './profile.js';
import type {
DevtoolsConfigOptions,
Expand Down Expand Up @@ -79,14 +75,12 @@ interface NormalizedConfigOptions {
export function createConfig(options: DevtoolsConfigOptions): Linter.Config[] {
const normalized = normalizeOptions(options);
const profile = resolveEslintProfile(options);
const packageEntrypoints = resolvePackageEntrypointFiles(options);

return defineConfig(
{ ignores: [...defaultIgnores, ...normalized.additionalIgnores] },
{ ...js.configs.recommended, files: normalized.files },
...createTypeCheckedConfigs(normalized),
createBaseConfig(normalized),
createModuleOwnershipConfig(normalized.files, packageEntrypoints),
...createProfileConfigs(profile, normalized.files),
...normalized.overrides,
...(normalized.includePrettier ? [prettierConfig] : []),
Expand Down
63 changes: 1 addition & 62 deletions src/tools/eslint/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ type LintResult = Awaited<ReturnType<ESLint['lintFiles']>>[number];
interface LintWorkspace {
readonly root: string;
lint(code: string, fileName: string, fix?: boolean): Promise<LintResult>;
lintFile(fileName: string, fix?: boolean): Promise<LintResult>;
write(code: string, fileName: string): Promise<void>;
}

async function createLintWorkspace(
Expand Down Expand Up @@ -58,16 +56,9 @@ async function createLintWorkspace(
await write(code, fileName);
return lintFile(fileName, fix);
},
lintFile,
write,
};
}

async function lintFresh(code: string, fileName: string): Promise<LintResult> {
const workspace = await createLintWorkspace();
return workspace.lint(code, fileName);
}

function ruleIds(result: LintResult): string[] {
expect(result.messages.filter((message) => message.fatal === true)).toEqual([]);
return result.messages.flatMap((message) => (message.ruleId === null ? [] : [message.ruleId]));
Expand Down Expand Up @@ -109,63 +100,11 @@ it('executes profile-specific React and React Native rules', async () => {
expect(ruleIds(nativeResult)).toContain('react-native/no-inline-styles');
});

it('keeps export sorting active inside index barrels', async () => {
it('keeps export sorting active', async () => {
const workspace = await createLintWorkspace();
const source = "export { z } from './z';\nexport { a } from './a';\n";
const result = await workspace.lint(source, 'index.ts');
expect(ruleIds(result)).toContain('simple-import-sort/exports');
expect(ruleIds(result)).not.toContain('ankhorage/no-forward-exports');
});

it('allows forward exports from declared non-index package entrypoints', async () => {
const workspace = await createLintWorkspace();
await writeFile(
path.join(workspace.root, 'package.json'),
JSON.stringify({
main: './dist/root.js',
types: './dist/root.d.ts',
exports: {
'./binding': {
types: './dist/bindingAuthoringModel.d.ts',
import: './dist/bindingAuthoringModel.js',
},
},
}),
);

await Promise.all([
workspace.write("export * from './index';\n", 'src/root.ts'),
workspace.write("export { value } from './value';\n", 'src/bindingAuthoringModel.ts'),
workspace.write("export * from './value';\n", 'src/implementation.ts'),
]);

const root = await workspace.lintFile('src/root.ts');
const binding = await workspace.lintFile('src/bindingAuthoringModel.ts');
const undeclared = await workspace.lintFile('src/implementation.ts');

expect(ruleIds(root)).not.toContain('ankhorage/no-forward-exports');
expect(ruleIds(binding)).not.toContain('ankhorage/no-forward-exports');
expect(ruleIds(undeclared)).toContain('ankhorage/no-forward-exports');
});

it('rejects named, type, and star forward exports outside index barrels', async () => {
const named = await lintFresh("export { value } from './value';\n", 'named.ts');
const typed = await lintFresh("export type { Value } from './value';\n", 'typed.ts');
const star = await lintFresh("export * from './value';\n", 'star.ts');

expect(ruleIds(named)).toContain('ankhorage/no-forward-exports');
expect(ruleIds(typed)).toContain('ankhorage/no-forward-exports');
expect(ruleIds(star)).toContain('ankhorage/no-forward-exports');
});

it('allows declarations exported where they are defined and index barrel forward exports', async () => {
const value = await lintFresh('export const value = 1;\n', 'owned.ts');
const type = await lintFresh('export type Value = string;\n', 'owned-type.ts');
const barrel = await lintFresh("export { value } from './value';\n", 'index.ts');

expect(ruleIds(value)).not.toContain('ankhorage/no-forward-exports');
expect(ruleIds(type)).not.toContain('ankhorage/no-forward-exports');
expect(ruleIds(barrel)).not.toContain('ankhorage/no-forward-exports');
});

it('keeps the central TypeScript, unused-import, formatting, and restricted-import rules active', async () => {
Expand Down
76 changes: 0 additions & 76 deletions src/tools/eslint/moduleOwnership.ts

This file was deleted.

53 changes: 0 additions & 53 deletions src/tools/eslint/packageEntrypoints.ts

This file was deleted.

Loading