diff --git a/eslint.config.mjs b/eslint.config.mjs index 3982a4b7..c705c433 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -46,6 +46,17 @@ const removedChatStateEscapeHatchRestrictions = [ message: "Use a named ChatAction instead of reintroducing the generic state patch escape hatch.", }, ]; +const handWrittenReExportRestrictions = [ + { + selector: "ExportNamedDeclaration[source.value]", + message: "Do not add hand-written re-exports. Import from the owning module directly unless this file is an explicit public boundary.", + }, + { + selector: "ExportAllDeclaration[source.value]", + message: + "Do not add hand-written export-all barrels. Import from the owning module directly unless this file is an explicit public boundary.", + }, +]; const chatSignalAdapterRestrictions = [ { selector: "ImportDeclaration[source.value='@preact/signals']", @@ -209,6 +220,7 @@ const appServerClientProjectionTypeRestrictions = [ }, ]; const baseSourceSyntaxRestrictions = [ + ...handWrittenReExportRestrictions, ...removedChatStateEscapeHatchRestrictions, ...generatedAppServerThreadImportRestrictions, ...appServerClientProjectionTypeRestrictions, @@ -295,7 +307,11 @@ export default defineConfig([ files: ["tests/**/*.{ts,tsx}"], rules: { ...testTypeScriptRelaxations, - ...restrictedSyntaxRule([...generatedAppServerThreadImportRestrictions, ...chatSignalAdapterRestrictions]), + ...restrictedSyntaxRule([ + ...handWrittenReExportRestrictions, + ...generatedAppServerThreadImportRestrictions, + ...chatSignalAdapterRestrictions, + ]), }, }, { diff --git a/tests/scripts/eslint-config.test.ts b/tests/scripts/eslint-config.test.ts index 2eb48411..b1db932d 100644 --- a/tests/scripts/eslint-config.test.ts +++ b/tests/scripts/eslint-config.test.ts @@ -101,6 +101,46 @@ export const status = signal("idle"); expect(messages).toContain("no-restricted-syntax"); }); + it("reports hand-written named re-exports", async () => { + const messages = await lintSource( + "src/shared/text/preview.ts", + ` +export { shortThreadId } from "../id/thread-id"; +export type { Thread } from "../../domain/threads/model"; +`, + ); + + expect(messages.filter((message) => message === "no-restricted-syntax")).toHaveLength(2); + }); + + it("reports hand-written export-all barrels", async () => { + const messages = await lintSource( + "src/shared/text/preview.ts", + ` +export * from "../id/thread-id"; +`, + ); + + expect(messages).toContain("no-restricted-syntax"); + }); + + it("allows local exports from owning modules", async () => { + const messages = await lintSource( + "src/shared/text/preview.ts", + ` +const limit = 80; + +export function truncate(value: string): string { + return value.slice(0, limit); +} + +export { limit }; +`, + ); + + expect(messages).not.toContain("no-restricted-syntax"); + }); + it("reports non-turn app-server protocol imports outside app-server", async () => { const messages = await lintSource( "src/features/chat/application/pending-requests/pending-request-actions.ts",