diff --git a/apps/src/tests/.eslintrc.js b/apps/src/tests/.eslintrc.js new file mode 100644 index 0000000000..b71b7f1ac6 --- /dev/null +++ b/apps/src/tests/.eslintrc.js @@ -0,0 +1,30 @@ +const path = require('path'); +const Module = require('module'); + +const pluginPath = require.resolve( + path.join(__dirname, 'eslint-plugin-local-rules'), +); +const originalResolve = Module._resolveFilename; +Module._resolveFilename = function (request, ...args) { + if (request === 'eslint-plugin-local-rules') { + return pluginPath; + } + return originalResolve.call(this, request, ...args); +}; + +module.exports = { + overrides: [ + { + files: [ + 'single-feature-tests/**/*.tsx', + 'single-feature-tests/**/*.ts', + 'component-integration-tests/**/*.tsx', + 'component-integration-tests/**/*.ts', + ], + plugins: ['local-rules'], + rules: { + 'local-rules/require-top-level-exports': 'error', + }, + }, + ], +}; diff --git a/apps/src/tests/component-integration-tests/index.tsx b/apps/src/tests/component-integration-tests/index.tsx index 8b4faa9fb2..e9219a914d 100644 --- a/apps/src/tests/component-integration-tests/index.tsx +++ b/apps/src/tests/component-integration-tests/index.tsx @@ -20,7 +20,7 @@ type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & { Home: undefined; }; -function HomeScreen() { +export function HomeScreen() { return ( {Object.entries(COMPONENT_SCENARIOS).map(([key, scenarioGroup]) => ( diff --git a/apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx b/apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx index bedca62897..fc4934bd8b 100644 --- a/apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx +++ b/apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx @@ -24,7 +24,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios'], }; -function ConfigScreen() { +export function ConfigScreen() { const { routeKey: tabRouteKey, routeOptions: tabRouteOptions, @@ -70,7 +70,7 @@ const STACK_ROUTE_CONFIGS: StackRouteConfig[] = [ }, ]; -function StackScreen() { +export function StackScreen() { return ; } diff --git a/apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx b/apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx index e721cb49a6..9c766de2b1 100644 --- a/apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx +++ b/apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx @@ -24,7 +24,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios'], }; -function ConfigScreen() { +export function ConfigScreen() { const { routeKey: stackRouteKey, routeOptions: stackRouteOptions, @@ -81,7 +81,7 @@ const TAB_ROUTE_CONFIGS: TabRouteConfig[] = [ }, ]; -function TabsScreen() { +export function TabsScreen() { return ; } diff --git a/apps/src/tests/eslint-plugin-local-rules/index.js b/apps/src/tests/eslint-plugin-local-rules/index.js new file mode 100644 index 0000000000..8109683ec8 --- /dev/null +++ b/apps/src/tests/eslint-plugin-local-rules/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'require-top-level-exports': require('./require-top-level-exports'), + }, +}; diff --git a/apps/src/tests/eslint-plugin-local-rules/package.json b/apps/src/tests/eslint-plugin-local-rules/package.json new file mode 100644 index 0000000000..f10e3a2822 --- /dev/null +++ b/apps/src/tests/eslint-plugin-local-rules/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-local-rules", + "version": "1.0.0", + "private": true, + "main": "index.js" +} diff --git a/apps/src/tests/eslint-plugin-local-rules/require-top-level-exports.js b/apps/src/tests/eslint-plugin-local-rules/require-top-level-exports.js new file mode 100644 index 0000000000..cd62d914e7 --- /dev/null +++ b/apps/src/tests/eslint-plugin-local-rules/require-top-level-exports.js @@ -0,0 +1,101 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'Require top-level React component declarations to be exported', + }, + schema: [], + messages: { + requireExport: 'Top-level component "{{name}}" must be exported.', + }, + }, + create(context) { + return { + Program(node) { + const exportedNames = collectExportedNames(node); + + for (const stmt of node.body) { + const name = getComponentName(stmt); + if (name === null) { + continue; + } + + if (!exportedNames.has(name)) { + context.report({ + node: stmt, + messageId: 'requireExport', + data: { name }, + }); + } + } + }, + }; + }, +}; + +function isPascalCase(name) { + return /^[A-Z]/.test(name); +} + +function getComponentName(stmt) { + if ( + stmt.type === 'FunctionDeclaration' && + stmt.id?.name && + isPascalCase(stmt.id.name) + ) { + return stmt.id.name; + } + + if (stmt.type === 'VariableDeclaration' && stmt.declarations.length === 1) { + const decl = stmt.declarations[0]; + const name = decl.id?.name; + if ( + name && + isPascalCase(name) && + decl.init && + (decl.init.type === 'ArrowFunctionExpression' || + decl.init.type === 'FunctionExpression') + ) { + return name; + } + } + + return null; +} + +function collectExportedNames(program) { + const names = new Set(); + + for (const stmt of program.body) { + if (stmt.type === 'ExportNamedDeclaration') { + if (stmt.declaration) { + for (const name of getDeclaredNames(stmt.declaration)) { + names.add(name); + } + } + for (const specifier of stmt.specifiers) { + names.add(specifier.local.name); + } + } + + if (stmt.type === 'ExportDefaultDeclaration') { + const decl = stmt.declaration; + if (decl.type === 'Identifier') { + names.add(decl.name); + } else if (decl.id) { + names.add(decl.id.name); + } + } + } + + return names; +} + +function getDeclaredNames(stmt) { + if (stmt.type === 'VariableDeclaration') { + return stmt.declarations.map(d => d.id?.name).filter(Boolean); + } + return stmt.id?.name ? [stmt.id.name] : []; +} diff --git a/apps/src/tests/single-feature-tests/index.tsx b/apps/src/tests/single-feature-tests/index.tsx index b7af64340e..1c3b257d61 100644 --- a/apps/src/tests/single-feature-tests/index.tsx +++ b/apps/src/tests/single-feature-tests/index.tsx @@ -24,7 +24,7 @@ type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & { Home: undefined; }; -function HomeScreen() { +export function HomeScreen() { return ( diff --git a/apps/src/tests/single-feature-tests/scroll-view-marker/test-svm-configures-scroll-view.tsx b/apps/src/tests/single-feature-tests/scroll-view-marker/test-svm-configures-scroll-view.tsx index 92570c9a99..a2827a8ab4 100644 --- a/apps/src/tests/single-feature-tests/scroll-view-marker/test-svm-configures-scroll-view.tsx +++ b/apps/src/tests/single-feature-tests/scroll-view-marker/test-svm-configures-scroll-view.tsx @@ -33,7 +33,7 @@ export function App() { ); } -function ContentScreen() { +export function ContentScreen() { return ( ; }) { diff --git a/apps/src/tests/single-feature-tests/split/test-top-column-for-collapsing.tsx b/apps/src/tests/single-feature-tests/split/test-top-column-for-collapsing.tsx index b655be86b2..9ad2561b6d 100644 --- a/apps/src/tests/single-feature-tests/split/test-top-column-for-collapsing.tsx +++ b/apps/src/tests/single-feature-tests/split/test-top-column-for-collapsing.tsx @@ -30,7 +30,7 @@ export function App() { ); } -function ColumnContent(props: { columnTitle: string }) { +export function ColumnContent(props: { columnTitle: string }) { return ( {props.columnTitle} diff --git a/apps/src/tests/single-feature-tests/stack-v4/stack-v4-orientation.tsx b/apps/src/tests/single-feature-tests/stack-v4/stack-v4-orientation.tsx index a249ae6ba0..f961e4e211 100644 --- a/apps/src/tests/single-feature-tests/stack-v4/stack-v4-orientation.tsx +++ b/apps/src/tests/single-feature-tests/stack-v4/stack-v4-orientation.tsx @@ -19,7 +19,7 @@ type StackParamList = { Screen1: undefined; }; -function ConfigScreen() { +export function ConfigScreen() { const [config, dispatch] = useStackConfigState(); return ( diff --git a/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-nested-stack.tsx b/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-nested-stack.tsx index 15dc897dc9..9945427417 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-nested-stack.tsx +++ b/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-nested-stack.tsx @@ -27,7 +27,7 @@ export function App() { ); } -function StackSetup() { +export function StackSetup() { const toast = useToast(); return ( @@ -77,7 +77,7 @@ function StackSetup() { ); } -function HomeScreen() { +export function HomeScreen() { return ( @@ -89,7 +89,7 @@ function HomeScreen() { ); } -function AScreen() { +export function AScreen() { return ( @@ -102,7 +102,7 @@ function AScreen() { ); } -function BScreen() { +export function BScreen() { return ( @@ -116,7 +116,7 @@ function BScreen() { ); } -function NestedStackScreen() { +export function NestedStackScreen() { const toast = useToast(); return ( @@ -168,7 +168,7 @@ function NestedStackScreen() { ); } -function NestedHomeScreen() { +export function NestedHomeScreen() { return ( @@ -182,7 +182,7 @@ function NestedHomeScreen() { ); } -function NestedAScreen() { +export function NestedAScreen() { return ( @@ -195,7 +195,7 @@ function NestedAScreen() { ); } -function NestedBScreen() { +export function NestedBScreen() { return ( @@ -209,7 +209,7 @@ function NestedBScreen() { ); } -function RouteInformation(props: { routeName: string }) { +export function RouteInformation(props: { routeName: string }) { const routeKey = useStackNavigationContext().routeKey; return ( @@ -220,7 +220,7 @@ function RouteInformation(props: { routeName: string }) { ); } -function TogglePreventNativeDismiss() { +export function TogglePreventNativeDismiss() { const navigation = useStackNavigationContext(); return ( @@ -235,7 +235,7 @@ function TogglePreventNativeDismiss() { ); } -function PreventNativeDismissInfo() { +export function PreventNativeDismissInfo() { const navContext = useStackNavigationContext(); return ( diff --git a/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-single-stack.tsx b/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-single-stack.tsx index 100e1f7a00..0e1419b61f 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-single-stack.tsx +++ b/apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-single-stack.tsx @@ -27,7 +27,7 @@ export function App() { ); } -function StackSetup() { +export function StackSetup() { const toast = useToast(); return ( @@ -69,7 +69,7 @@ function StackSetup() { ); } -function HomeScreen() { +export function HomeScreen() { return ( @@ -78,7 +78,7 @@ function HomeScreen() { ); } -function AScreen() { +export function AScreen() { return ( @@ -88,7 +88,7 @@ function AScreen() { ); } -function BScreen() { +export function BScreen() { return ( @@ -99,7 +99,7 @@ function BScreen() { ); } -function RouteInformation(props: { routeName: string }) { +export function RouteInformation(props: { routeName: string }) { const routeKey = useStackNavigationContext().routeKey; return ( @@ -110,7 +110,7 @@ function RouteInformation(props: { routeName: string }) { ); } -function TogglePreventNativeDismiss() { +export function TogglePreventNativeDismiss() { const navigation = useStackNavigationContext(); return ( @@ -125,7 +125,7 @@ function TogglePreventNativeDismiss() { ); } -function PreventNativeDismissInfo() { +export function PreventNativeDismissInfo() { const navContext = useStackNavigationContext(); return ( diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-animation-android.tsx b/apps/src/tests/single-feature-tests/stack-v5/test-animation-android.tsx index 85cc972ae8..6521a1e999 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/test-animation-android.tsx +++ b/apps/src/tests/single-feature-tests/stack-v5/test-animation-android.tsx @@ -17,7 +17,7 @@ export function App() { return ; } -function StackSetup() { +export function StackSetup() { return ( ; } -function StackSetup() { +export function StackSetup() { return ( @@ -54,7 +54,7 @@ function HomeScreen() { ); } -function AScreen() { +export function AScreen() { return ( @@ -63,7 +63,7 @@ function AScreen() { ); } -function BScreen() { +export function BScreen() { return ( @@ -72,7 +72,7 @@ function BScreen() { ); } -function RouteInformation(props: { routeName: string }) { +export function RouteInformation(props: { routeName: string }) { const routeKey = useStackNavigationContext().routeKey; return ( diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-android/index.tsx b/apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-android/index.tsx index cf5bb00e00..98dfe9149a 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-android/index.tsx +++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-android/index.tsx @@ -164,7 +164,7 @@ export function App() { return ; } -function StackSetup() { +export function StackSetup() { return ( (DEFAULT_CONFIG); diff --git a/apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx b/apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx index 98a9f14ccd..f0770d2e32 100644 --- a/apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx +++ b/apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx @@ -18,7 +18,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios'], }; -function ShortViewUL() { +export function ShortViewUL() { return ( @@ -62,7 +62,7 @@ function LongView() { ); } -function RGBView() { +export function RGBView() { return ( @@ -80,7 +80,7 @@ const ACCESSORY_VARIANTS = [ { id: 4, content: RGBView }, ]; -function ConfigScreen() { +export function ConfigScreen() { const [selected, setSelected] = useState(0); const { updateHostConfig } = useTabsHostConfig(); diff --git a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx b/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx index a1218a3161..f7a603f450 100644 --- a/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx +++ b/apps/src/tests/single-feature-tests/tabs/override-scroll-view-content-inset.tsx @@ -19,7 +19,7 @@ const scenarioDescription: ScenarioDescription = { const ITEM_COUNT = 30; -function ScrollContent({ label }: { label: string }) { +export function ScrollContent({ label }: { label: string }) { return ( @@ -36,15 +36,15 @@ function ScrollContent({ label }: { label: string }) { ); } -function FalseTab() { +export function FalseTab() { return ; } -function TrueTab() { +export function TrueTab() { return ; } -function DefaultTab() { +export function DefaultTab() { return ; } diff --git a/apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx b/apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx index 3c3c34d505..7b94a0e7ba 100644 --- a/apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx +++ b/apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx @@ -17,7 +17,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios', 'android'], }; -function ConfigScreen() { +export function ConfigScreen() { const { routeKey, routeOptions, setRouteOptions } = useTabsNavigationContext(); diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-appearance-defined-by-selected-tab.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-appearance-defined-by-selected-tab.tsx index 3e00d67293..327ab6b341 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-appearance-defined-by-selected-tab.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-appearance-defined-by-selected-tab.tsx @@ -66,7 +66,7 @@ const DEFAULT_APPEARANCE_IOS: TabsScreenAppearanceIOS = { }, }; -function TabScreen() { +export function TabScreen() { return ( Tab diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx index 774b2e7c42..fda338a6f9 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx @@ -20,7 +20,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['android'], }; -function ConfigScreen() { +export function ConfigScreen() { const { routeKey, routeOptions, setRouteOptions } = useTabsNavigationContext(); const { hostConfig, updateHostConfig } = useTabsHostConfig(); diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-more-navigation-controller.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-more-navigation-controller.tsx index a8adcad868..e543d292cc 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-more-navigation-controller.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-more-navigation-controller.tsx @@ -20,7 +20,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios'], }; -function ContentView() { +export function ContentView() { const { routeKey } = useTabsNavigationContext(); return ( @@ -32,7 +32,7 @@ function ContentView() { ); } -function TabsNavigationButtons() { +export function TabsNavigationButtons() { const nav = useTabsNavigationContext(); return ( @@ -88,7 +88,7 @@ export function App() { ); } -function AppContents() { +export function AppContents() { const toast = useToast(); return ( diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-prevent-native-selection/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-prevent-native-selection/index.tsx index 13273e1470..38d3570613 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-prevent-native-selection/index.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-prevent-native-selection/index.tsx @@ -20,7 +20,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['android', 'ios'], }; -function ContentView() { +export function ContentView() { const nav = useTabsNavigationContext(); const preventNativeSelection = @@ -47,7 +47,7 @@ function ContentView() { ); } -function TabsNavigationButtons() { +export function TabsNavigationButtons() { const nav = useTabsNavigationContext(); return ( @@ -114,7 +114,7 @@ export function App() { ); } -function AppContents() { +export function AppContents() { const toast = useToast(); return ( diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-simple-nav.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-simple-nav.tsx index 76a58829f9..c44a84295d 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-simple-nav.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-simple-nav.tsx @@ -17,7 +17,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['android', 'ios'], }; -function ContentView() { +export function ContentView() { const { routeKey } = useTabsNavigationContext(); return ( @@ -29,7 +29,7 @@ function ContentView() { ); } -function TabsNavigationButtons() { +export function TabsNavigationButtons() { const nav = useTabsNavigationContext(); return ( diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-stale-update-rejection.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-stale-update-rejection.tsx index dbafdad6d0..d20590a138 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-stale-update-rejection.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-stale-update-rejection.tsx @@ -20,7 +20,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['android', 'ios'], }; -function ContentView() { +export function ContentView() { const { routeKey } = useTabsNavigationContext(); const { hostConfig, updateHostConfig } = useTabsHostConfig(); @@ -58,7 +58,7 @@ function ContentView() { ); } -function TabsNavigationButtons() { +export function TabsNavigationButtons() { const nav = useTabsNavigationContext(); return ( @@ -102,7 +102,7 @@ export function App() { ); } -function AppContents() { +export function AppContents() { const toast = useToast(); return ( @@ -122,7 +122,7 @@ function AppContents() { ); } -function HeavyRenderHierarchy({ +export function HeavyRenderHierarchy({ enabled, timeMs = 5000, }: { diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-color-scheme/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-color-scheme/index.tsx index 4575505867..4d4a482ddc 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-color-scheme/index.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-color-scheme/index.tsx @@ -27,7 +27,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['android', 'ios'], }; -function ConfigScreen() { +export function ConfigScreen() { const { hostConfig, updateHostConfig } = useTabsHostConfig(); const [reactColorScheme, setReactColorScheme] = React.useState('unspecified'); @@ -78,7 +78,7 @@ function ConfigScreen() { ); } -function TestScreen() { +export function TestScreen() { return ( diff --git a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-controller-mode-ios/index.tsx b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-controller-mode-ios/index.tsx index f4ad19efdd..6980197665 100644 --- a/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-controller-mode-ios/index.tsx +++ b/apps/src/tests/single-feature-tests/tabs/test-tabs-tab-bar-controller-mode-ios/index.tsx @@ -18,7 +18,7 @@ const scenarioDescription: ScenarioDescription = { platforms: ['ios'], }; -function ConfigScreen() { +export function ConfigScreen() { const { hostConfig, updateHostConfig } = useTabsHostConfig(); return ( @@ -41,7 +41,7 @@ function ConfigScreen() { ); } -function TestScreen() { +export function TestScreen() { return ( @@ -40,7 +40,7 @@ function ConfigScreen() { ); } -function TestScreen() { +export function TestScreen() { return (