Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/eslint-plugin-add-promise-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/eslint-plugin': patch
---

Handle non-promise return types when `require-auth-protection` rule fixer makes the function `async`.

The eslint rule fixer will now wrap a non-promise return type with `Promise<>` to produce valid TypeScript.
7 changes: 7 additions & 0 deletions .changeset/eslint-plugin-fixer-quote-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/eslint-plugin': patch
---

The `require-auth-protection` fixer now matches the string quote style of existing imports when inserting a new `auth` import.

Previously, new imports always used single quotes regardless of how other imports in the file were quoted.
7 changes: 7 additions & 0 deletions .changeset/eslint-plugin-or-auth-guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/eslint-plugin': patch
---

The `require-auth-protection` rule now accepts OR-conditions like `if (!isAuthenticated || otherCondition)` when determining if a resource is protected.

Previously, only bare auth checks such as `if (!isAuthenticated)` were recognized. Guards with only `||` are safe but were incorrectly reported as missing protection.
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default async function Page() {
expect(result.unresolved).toEqual([]);
expect(result.fixed).toEqual([{ filePath: page, protections: 1 }]);

expect(await readFile(page, 'utf8')).toBe(`import { auth } from '@clerk/nextjs/server';
expect(await readFile(page, 'utf8')).toBe(`import { auth } from "@clerk/nextjs/server";
export default async function Page() {
await auth.protect();
return <div>Hello</div>;
Expand Down Expand Up @@ -109,7 +109,7 @@ export async function POST() {
expect(result.fixed).toEqual([{ filePath: route, protections: 2 }]);

const output = await readFile(route, 'utf8');
expect(output).toBe(`import { auth } from '@clerk/nextjs/server';
expect(output).toBe(`import { auth } from "@clerk/nextjs/server";
export async function GET() {
await auth.protect();
return new Response('ok');
Expand Down
44 changes: 44 additions & 0 deletions packages/eslint-plugin/src/next/__tests__/quote-style.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as tsParser from '@typescript-eslint/parser';
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
import { describe, expect, it } from 'vitest';

import { inferQuoteChar } from '../lib/quote-style';

function parse(code: string): { sourceCode: TSESLint.SourceCode; program: TSESTree.Program } {
const program = tsParser.parse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true },
}) as TSESTree.Program;
const sourceCode = {
ast: program,
getText(node: TSESTree.Node) {
return code.slice(node.range[0], node.range[1]);
},
} as TSESLint.SourceCode;
return { sourceCode, program };
}

describe('inferQuoteChar', () => {
it('returns double quotes from a double-quoted import', () => {
const { sourceCode, program } = parse('import { x } from "@pkg/foo";');
expect(inferQuoteChar(sourceCode, program)).toBe('"');
});

it('returns single quotes from a single-quoted import', () => {
const { sourceCode, program } = parse("import { x } from '@pkg/foo';");
expect(inferQuoteChar(sourceCode, program)).toBe("'");
});

it('returns double quotes from a double-quoted export source', () => {
const { sourceCode, program } = parse('export { GET } from "./route";');
expect(inferQuoteChar(sourceCode, program)).toBe('"');
});

it('falls back to double quotes when the file has no module sources to infer from', () => {
const { sourceCode, program } = parse(`export default function Page() {
return <div />;
}`);
expect(inferQuoteChar(sourceCode, program)).toBe('"');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ ruleTester.run('require-auth-protection (suggestions)', rule, {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export default async function Page() {
await auth.protect();
return <div>Hello</div>;
Expand All @@ -65,7 +65,7 @@ export default async function Page() {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export default async () => {
await auth.protect();
return null;
Expand All @@ -86,7 +86,7 @@ export default async () => {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export const GET = async () => {
await auth.protect();
return { ok: true };
Expand All @@ -107,7 +107,7 @@ export const GET = async () => {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export default async () => {
await auth.protect();
return <div>Hello</div>;
Expand All @@ -132,7 +132,7 @@ export default Page;`,
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
async function Page() {
await auth.protect();
return <div>Hello</div>;
Expand All @@ -159,7 +159,7 @@ export const POST = () => new Response('ok');`,
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export async function GET() {
await auth.protect();
return new Response('ok');
Expand All @@ -174,7 +174,7 @@ export const POST = () => new Response('ok');`,
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export async function GET() {
return new Response('ok');
}
Expand Down Expand Up @@ -204,7 +204,7 @@ export async function loadData() {
{
messageId: 'addAuthProtect',
output: `'use server';
import { auth } from '@clerk/nextjs/server';
import { auth } from "@clerk/nextjs/server";

export async function loadData() {
await auth.protect();
Expand All @@ -229,7 +229,7 @@ export async function loadData() {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export async function action() {
'use server';
await auth.protect();
Expand All @@ -254,7 +254,7 @@ export async function action() {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
const action = async function () {
'use server';
await auth.protect();
Expand Down Expand Up @@ -282,7 +282,7 @@ const action = async function () {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";
export function getAction() {
const create = async () => {
'use server';
Expand Down Expand Up @@ -339,6 +339,59 @@ export default function Page() {
messageId: 'addAuthProtect',
output: `import { currentUser, auth } from '@clerk/nextjs/server';

export default async function Page() {
await auth.protect();
return null;
}`,
},
],
},
],
},
{
name: 'double-quoted clerk import without auth: merges the specifier without changing quote style',
code: `import { currentUser } from "@clerk/nextjs/server";

export default function Page() {
return null;
}`,
filename: abs('app/dashboard/page.tsx'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { currentUser, auth } from "@clerk/nextjs/server";

export default async function Page() {
await auth.protect();
return null;
}`,
},
],
},
],
},
{
name: 'double-quoted import in file: new auth import matches file quote style',
code: `import { redirect } from "next/navigation";

export default function Page() {
return null;
}`,
filename: abs('app/dashboard/page.tsx'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";

export default async function Page() {
await auth.protect();
return null;
Expand Down Expand Up @@ -378,7 +431,7 @@ export default async function Page() {
},
{
name: 'existing await auth() destructure: merges .protect() into the call',
code: `import { auth } from '@clerk/nextjs/server';
code: `import { auth } from "@clerk/nextjs/server";

export default async function Page() {
const { userId } = await auth();
Expand All @@ -392,7 +445,7 @@ export default async function Page() {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";

export default async function Page() {
const { userId } = await auth.protect();
Expand All @@ -405,7 +458,7 @@ export default async function Page() {
},
{
name: 'existing bare await auth(): merges .protect() into the call',
code: `import { auth } from '@clerk/nextjs/server';
code: `import { auth } from "@clerk/nextjs/server";

export async function GET() {
await auth();
Expand All @@ -419,7 +472,7 @@ export async function GET() {
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";

export async function GET() {
await auth.protect();
Expand All @@ -432,7 +485,7 @@ export async function GET() {
},
{
name: 'concise arrow awaiting auth(): merges .protect() into the call',
code: `import { auth } from '@clerk/nextjs/server';
code: `import { auth } from "@clerk/nextjs/server";

export const POST = async () => await auth();`,
filename: abs('app/api/widgets/route.ts'),
Expand All @@ -443,7 +496,7 @@ export const POST = async () => await auth();`,
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from '@clerk/nextjs/server';
output: `import { auth } from "@clerk/nextjs/server";

export const POST = async () => await auth.protect();`,
},
Expand Down Expand Up @@ -472,6 +525,96 @@ export default async function Page() {
export default async function Page() {
const { userId } = await clerkAuth.protect();
return <div>{userId}</div>;
}`,
},
],
},
],
},
{
name: 'page: sync default export with explicit return type — wraps in Promise',
code: `export default function Page(): JSX.Element {
return <div>Hello</div>;
}`,
filename: abs('app/dashboard/page.tsx'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from "@clerk/nextjs/server";
export default async function Page(): Promise<JSX.Element> {
await auth.protect();
return <div>Hello</div>;
}`,
},
],
},
],
},
{
name: 'route: sync arrow with explicit return type — wraps in Promise',
code: `export const GET = (): Response => new Response('ok');`,
filename: abs('app/api/widgets/route.ts'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from "@clerk/nextjs/server";
export const GET = async (): Promise<Response> => {
await auth.protect();
return new Response('ok');
};`,
},
],
},
],
},
{
name: 'route: sync function with Promise return type — does not double-wrap',
code: `export function GET(): Promise<Response> {
return Promise.resolve(new Response('ok'));
}`,
filename: abs('app/api/widgets/route.ts'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from "@clerk/nextjs/server";
export async function GET(): Promise<Response> {
await auth.protect();
return Promise.resolve(new Response('ok'));
}`,
},
],
},
],
},
{
name: 'route: type-predicate return type — adds async but does not wrap return type',
code: `export function GET(value: unknown): value is boolean {
return typeof value === 'boolean';
}`,
filename: abs('app/api/widgets/route.ts'),
options: [config],
errors: [
{
messageId: 'missingProtect',
suggestions: [
{
messageId: 'addAuthProtect',
output: `import { auth } from "@clerk/nextjs/server";
export async function GET(value: unknown): value is boolean {
await auth.protect();
return typeof value === 'boolean';
}`,
},
],
Expand Down
Loading
Loading