Skip to content

Commit c41f2ab

Browse files
committed
Only match app/ and src/app/
1 parent 4d40ec8 commit c41f2ab

3 files changed

Lines changed: 45 additions & 12 deletions

File tree

‎packages/eslint-plugin/README.md‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ Note that the bulk auto-fixer described further down does require `eslint` to be
9595

9696
| Option | Type | Default | Description |
9797
| ------------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98-
| `protected` | `string[]` (required) | — | Project-relative folder globs whose resources must be guarded. |
99-
| `public` | `string[]` | `[]` | Project-relative folder globs that are exempt. |
98+
| `protected` | `string[]` (required) | — | Folder globs relative to the project root whose resources must be guarded. |
99+
| `public` | `string[]` | `[]` | Folder globs relative to the project root that are exempt. |
100100
| `resources` | `object` | all true | Resource groups to check. Supports `routeHandlers`, `serverFunctions`, and `serverComponentEntrypoints`, each as an optional boolean. |
101101
| `mixedScopeLayouts` | `'auto' \| string[]` | `'auto'` | Layouts/templates that intentionally wrap both protected and public descendants. `'auto'` allows them silently; a list requires each such folder to be acknowledged explicitly. |
102102
| `rootDir` | `string` | _(auto)_ | Project root used to resolve project-relative folder globs. Defaults to the nearest ancestor `eslint.config.*`, then ESLint `cwd`. Set to `import.meta.dirname` in your config file when auto-discovery is unavailable. |
@@ -107,12 +107,12 @@ Globs use a minimal dialect — only `*` (single segment) and `**` (any depth).
107107

108108
`protected` and `public` are project-relative folder globs. Use `app/**` for a root `app` directory, `src/app/**` for a `src/app` directory, and `src/**`, `shared/**`, etc. for other project folders.
109109

110-
The project root is normally determined by the nearest `eslint.config.*` ancestor of the file being checked, and falls back to `cwd`. If you need better control, for example in a monorepo setup, you can specify `rootDir`; the globs will be relative to that directory.
110+
The project root is normally determined by the nearest `eslint.config.*` ancestor of the file being checked, and falls back to `cwd`. You can configure it manually using `rootDir`.
111111

112112
We recommend starting with `protected: ['**']`. This protects the following resources by default:
113113

114-
- Server Functions are checked wherever they live in the project
115-
- Regardless of configuration, App Router entrypoints like `page.jsx` or `route.js` are only checked if they live under a `app/` folder
114+
- Server Functions are checked wherever they live in the project when their folder is protected
115+
- Regardless of configuration, App Router entrypoints like `page.jsx` or `route.js` are only checked if they live under `app/` or `src/app/` relative to the project root
116116

117117
Use `public` for explicit exemptions:
118118

@@ -165,6 +165,28 @@ Use `resources` to disable whole resource groups when a project only wants this
165165

166166
We recommend leaving all as true, but switching some off can be useful during incremental migrations. This configuration also scopes suggestions and bulk-fix tooling: disabled resource groups are not reported by the rule, so they will not receive editor quick-fixes or bulk-applied fixes.
167167

168+
## Monorepo setups
169+
170+
If you keep a separate `eslint.config.*` in each application, this rule will work with your monorepo setup out of the box.
171+
172+
If you have a single top level `eslint.config.*`, you need to define the rule once for each application, and you need to configure `rootDir` to point to each application root (for example `apps/web`). This makes sense when you consider that each `protected` and `public` pattern is application specific. For example:
173+
174+
```ts
175+
{
176+
files: ['apps/web/src/**/*.{ts,tsx}'],
177+
rules: {
178+
'@clerk/next/require-auth-protection': [
179+
'error',
180+
{
181+
rootDir: 'apps/web',
182+
protected: ['**'],
183+
public: ['src/app/sign-in/**'],
184+
}
185+
]
186+
}
187+
}
188+
```
189+
168190
## What counts as protected
169191

170192
The rule is satisfied when the relevant function guards itself at the top, either by calling `auth.protect()`:
@@ -216,7 +238,7 @@ npx clerk-next-fix-auth-protection --dry-run
216238
# Scope fixes to a specific pattern, this will still
217239
# use protected/public from your ESLint config, but
218240
# can be useful to only fix a subset of your application
219-
npx clerk-next-fix-auth-protection "src/app/**"
241+
npx clerk-next-fix-auth-protection "src/**"
220242
```
221243

222244
Resources the rule can't safely fix on its own (imported/wrapped exports, unacknowledged mixed-scope layouts) are listed as needing manual attention, and the command exits non-zero when any remain (or when `--dry-run` would make changes).
@@ -227,7 +249,7 @@ The same logic is available programmatically:
227249
import { fixAuthProtection } from '@clerk/eslint-plugin/next/fix-auth-protection';
228250

229251
const { fixed, unresolved } = await fixAuthProtection({
230-
patterns: ['src/app/**'],
252+
patterns: ['src/**'],
231253
dryRun: false,
232254
});
233255
```

‎packages/eslint-plugin/src/next/__tests__/file-info.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ describe('isUnderAppRouterRoot', () => {
6060
it('returns true for `app/` and `src/app/` folders', () => {
6161
expect(isUnderAppRouterRoot('app')).toBe(true);
6262
expect(isUnderAppRouterRoot('app/dashboard')).toBe(true);
63+
expect(isUnderAppRouterRoot('src/app')).toBe(true);
6364
expect(isUnderAppRouterRoot('src/app/dashboard')).toBe(true);
64-
expect(isUnderAppRouterRoot('apps/web/app/dashboard')).toBe(true);
6565
});
6666

67-
it('returns false for folders that merely contain `app` in a segment name', () => {
67+
it('returns false for folders that are not rooted at `app/` or `src/app/`', () => {
68+
expect(isUnderAppRouterRoot('apps/web/app/dashboard')).toBe(false);
69+
expect(isUnderAppRouterRoot('src/pages-router/app')).toBe(false);
70+
expect(isUnderAppRouterRoot('src/pages-router/app/dashboard')).toBe(false);
6871
expect(isUnderAppRouterRoot('myapp/dashboard')).toBe(false);
6972
expect(isUnderAppRouterRoot('app-utils')).toBe(false);
7073
expect(isUnderAppRouterRoot('utils')).toBe(false);
@@ -81,6 +84,8 @@ describe('getAppRouterFileKind', () => {
8184
it('returns null for resource filenames outside App Router', () => {
8285
expect(getAppRouterFileKind('/proj/utils/page.tsx', 'utils')).toBeNull();
8386
expect(getAppRouterFileKind('/proj/shared/route.ts', 'shared')).toBeNull();
87+
expect(getAppRouterFileKind('/proj/apps/web/app/page.tsx', 'apps/web/app')).toBeNull();
88+
expect(getAppRouterFileKind('/proj/src/pages-router/app/page.tsx', 'src/pages-router/app')).toBeNull();
8489
});
8590

8691
it('returns null for non-resource files even under App Router', () => {

‎packages/eslint-plugin/src/next/lib/file-info.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@ export function getRelativeFolder(filename: string | undefined, rootDir: string
3838
}
3939

4040
/**
41-
* Whether `relativeFolder` lies under a Next.js App Router root (`app/...` or
42-
* `src/app/...`, including nested monorepo layouts such as `apps/web/app/...`).
41+
* Whether `relativeFolder` lies under a Next.js App Router root, relative to
42+
* the configured project root. Only Next.js' supported root layouts (`app/...`
43+
* and `src/app/...`) count; monorepo apps should set `rootDir` per app.
4344
*/
4445
export function isUnderAppRouterRoot(relativeFolder: string): boolean {
45-
return relativeFolder.split('/').some(seg => seg === 'app');
46+
return (
47+
relativeFolder === 'app' ||
48+
relativeFolder.startsWith('app/') ||
49+
relativeFolder === 'src/app' ||
50+
relativeFolder.startsWith('src/app/')
51+
);
4652
}
4753

4854
/**

0 commit comments

Comments
 (0)