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
10 changes: 10 additions & 0 deletions .changeset/nextjs-dev-key-init-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@clerk/nextjs': minor
'@clerk/shared': minor
---

Print a one-time notice in the server terminal when `<ClerkProvider>` renders with a development publishable key, naming `npx clerk@latest init` as the way to get working keys without a Clerk account. The notice appears once per process, so once per build worker during `next build`, and on the first server render under `next dev`. It never prints in the browser, in deployed runtimes, or when the keys came from keyless mode. It is silenced by the existing `unsafe_disableDevelopmentModeConsoleWarning` prop or `NEXT_PUBLIC_CLERK_UNSAFE_DISABLE_DEVELOPMENT_MODE_CONSOLE_WARNING` env var.

Fix `unsafe_disableDevelopmentModeConsoleWarning` being ignored when passed as a prop to the Next.js `<ClerkProvider>`; previously only the env var took effect, so the prop did not silence the browser development-keys warning either.

`@clerk/shared/keys` now exports `accountlessInitGuidance`, the sentence used by this notice and by the existing missing-key errors.
43 changes: 43 additions & 0 deletions integration/tests/next-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,33 @@ export default function RootLayout({ children }: { children: React.ReactNode })
);
}
`,
)
.addFile(
'src/app/dev-key-notice/node/page.tsx',
() => `export const dynamic = 'force-dynamic';

export default function Page() {
console.log('dev-key-notice-sentinel:node');
return <p>dev-key-notice-marker:node</p>;
}
`,
)
.addFile(
'src/app/dev-key-notice/edge/page.tsx',
() => `export const runtime = 'edge';
export const dynamic = 'force-dynamic';

export default function Page() {
console.log('dev-key-notice-sentinel:edge');
return <p>dev-key-notice-marker:edge</p>;
}
`,
)
.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withEmailCodes);
await app.build();
await app.serve();
});

test.afterAll(async () => {
Expand All @@ -155,6 +177,27 @@ export default function RootLayout({ children }: { children: React.ReactNode })
expect(notFoundPageLine).toContain(staticIndicator);
});

test('Prints the clerk init hint for development keys when <ClerkProvider /> is a client component', () => {
expect(app.buildOutput).toContain('Development keys in use');
});

test('Does not print the clerk init hint when the built app is served', async () => {
// Both pages render the provider at request time, one on Node and one on Edge, and log a sentinel so
// the negative assertion below only runs once their server output has been captured.
for (const target of ['node', 'edge']) {
const res = await fetch(`${app.serverUrl}/dev-key-notice/${target}`);
expect(res.status).toBe(200);
expect(await res.text()).toContain(`dev-key-notice-marker:${target}`);
}
await expect
.poll(() => app.serveOutput, { timeout: 15_000 })
.toMatch(
/dev-key-notice-sentinel:node[\s\S]*dev-key-notice-sentinel:edge|dev-key-notice-sentinel:edge[\s\S]*dev-key-notice-sentinel:node/,
);

expect(app.serveOutput).not.toContain('Development keys in use');
});

/**
* Sometimes utilities from `/server` may use Node APIs even if `clerkMiddleware` does not consumes them.
* This happens because of code for node runtime and edge runtime is bundled together in the `/server/index.ts` barrel file.
Expand Down
7 changes: 7 additions & 0 deletions packages/nextjs/src/app-router/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEf
import { ClerkNextOptionsProvider, useClerkNextOptions } from '../../client-boundary/NextOptionsContext';
import { errorThrower } from '../../server/errorThrower';
import type { NextClerkProviderProps } from '../../types';
import { maybeShowDevelopmentKeyNotice } from '../../utils/devKeyNotice';
import { canUseKeyless } from '../../utils/feature-flags';
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
import { RouterTelemetry } from '../../utils/router-telemetry';
Expand Down Expand Up @@ -76,6 +77,12 @@ const NextClientClerkProvider = <TUi extends Ui = Ui>(props: NextClerkProviderPr
routerReplace: replace,
});

maybeShowDevelopmentKeyNotice({
publishableKey: mergedProps.publishableKey,
disabled: mergedProps.unsafe_disableDevelopmentModeConsoleWarning,
keyless: Boolean(mergedProps.__internal_keyless_claimKeylessApplicationUrl),
});

return (
<ClerkNextOptionsProvider options={mergedProps}>
<ReactClerkProvider {...mergedProps}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @vitest-environment node
*/
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { maybeShowDevelopmentKeyNotice } from '../../../utils/devKeyNotice';
import { ClientClerkProvider } from '../ClerkProvider';

vi.mock('next/navigation', () => ({
useRouter: () => ({ refresh: vi.fn(), push: vi.fn(), replace: vi.fn() }),
}));
vi.mock('../useAwaitablePush', () => ({ useAwaitablePush: () => vi.fn() }));
vi.mock('../useAwaitableReplace', () => ({ useAwaitableReplace: () => vi.fn() }));
vi.mock('../../server-actions', () => ({ invalidateCacheAction: vi.fn() }));
vi.mock('../ClerkScripts', () => ({ ClerkScripts: () => null }));
vi.mock('../../../utils/router-telemetry', () => ({ RouterTelemetry: () => null }));
vi.mock('@clerk/react/internal', () => ({
InternalClerkProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('../../../utils/devKeyNotice', () => ({ maybeShowDevelopmentKeyNotice: vi.fn() }));

const notice = maybeShowDevelopmentKeyNotice as unknown as ReturnType<typeof vi.fn>;
const DEV_KEY = 'pk_test_ZmFrZS1jbGVyay5hY2NvdW50cy5kZXYk';
const ORIGINAL_ENV = { ...process.env };

describe('ClientClerkProvider (server render)', () => {
beforeEach(() => {
delete process.env.NEXT_PUBLIC_CLERK_UNSAFE_DISABLE_DEVELOPMENT_MODE_CONSOLE_WARNING;
notice.mockClear();
});

afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});

it('asks for the development key notice with the resolved key', () => {
const html = renderToStaticMarkup(<ClientClerkProvider publishableKey={DEV_KEY}>child</ClientClerkProvider>);

expect(html).toContain('child');
expect(notice).toHaveBeenCalledTimes(1);
expect(notice).toHaveBeenCalledWith({ publishableKey: DEV_KEY, disabled: false, keyless: false });
});

it('passes the opt-out through when set as a prop', () => {
renderToStaticMarkup(
<ClientClerkProvider
publishableKey={DEV_KEY}
unsafe_disableDevelopmentModeConsoleWarning
>
child
</ClientClerkProvider>,
);

expect(notice).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
});

it('passes the opt-out through when set by env var', () => {
process.env.NEXT_PUBLIC_CLERK_UNSAFE_DISABLE_DEVELOPMENT_MODE_CONSOLE_WARNING = 'true';

renderToStaticMarkup(<ClientClerkProvider publishableKey={DEV_KEY}>child</ClientClerkProvider>);

expect(notice).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
});

it('flags keys that came from keyless mode', () => {
renderToStaticMarkup(
<ClientClerkProvider
publishableKey={DEV_KEY}
__internal_keyless_claimKeylessApplicationUrl='https://dashboard.clerk.com/claim'
>
child
</ClientClerkProvider>,
);

expect(notice).toHaveBeenCalledWith(expect.objectContaining({ keyless: true }));
});
});
6 changes: 6 additions & 0 deletions packages/nextjs/src/pages/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React from 'react';
import { useSafeLayoutEffect } from '../client-boundary/hooks/useSafeLayoutEffect';
import { ClerkNextOptionsProvider } from '../client-boundary/NextOptionsContext';
import type { NextClerkProviderProps } from '../types';
import { maybeShowDevelopmentKeyNotice } from '../utils/devKeyNotice';
import { invalidateNextRouterCache } from '../utils/invalidateNextRouterCache';
import { mergeNextClerkPropsWithEnv } from '../utils/mergeNextClerkPropsWithEnv';
import { removeBasePath } from '../utils/removeBasePath';
Expand Down Expand Up @@ -46,6 +47,11 @@ export function ClerkProvider<TUi extends Ui = Ui>({ children, ...props }: NextC
routerPush: navigate,
routerReplace: replaceNavigate,
});
maybeShowDevelopmentKeyNotice({
publishableKey: mergedProps.publishableKey,
disabled: mergedProps.unsafe_disableDevelopmentModeConsoleWarning,
keyless: Boolean(mergedProps.__internal_keyless_claimKeylessApplicationUrl),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// ClerkProvider automatically injects __clerk_ssr_state
// getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state
// @ts-expect-error initialState is hidden from the types as it's a private prop
Expand Down
70 changes: 70 additions & 0 deletions packages/nextjs/src/pages/__tests__/ClerkProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @vitest-environment node
*/
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { maybeShowDevelopmentKeyNotice } from '../../utils/devKeyNotice';
import { ClerkProvider } from '../ClerkProvider';

vi.mock('next/router', () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
vi.mock('../ClerkScripts', () => ({ ClerkScripts: () => null }));
vi.mock('../../utils/router-telemetry', () => ({ RouterTelemetry: () => null }));
vi.mock('@clerk/react/internal', () => ({
InternalClerkProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
setClerkJSLoadingErrorPackageName: vi.fn(),
setErrorThrowerOptions: vi.fn(),
}));
vi.mock('../../utils/devKeyNotice', () => ({ maybeShowDevelopmentKeyNotice: vi.fn() }));

const notice = maybeShowDevelopmentKeyNotice as unknown as ReturnType<typeof vi.fn>;
const DEV_KEY = 'pk_test_ZmFrZS1jbGVyay5hY2NvdW50cy5kZXYk';
const ORIGINAL_ENV = { ...process.env };

describe('Pages Router ClerkProvider (server render)', () => {
beforeEach(() => {
delete process.env.NEXT_PUBLIC_CLERK_UNSAFE_DISABLE_DEVELOPMENT_MODE_CONSOLE_WARNING;
notice.mockClear();
});

afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});

it('asks for the development key notice with the resolved key and opt-out', () => {
const html = renderToStaticMarkup(<ClerkProvider publishableKey={DEV_KEY}>child</ClerkProvider>);

expect(html).toContain('child');
expect(notice).toHaveBeenCalledTimes(1);
expect(notice).toHaveBeenCalledWith({ publishableKey: DEV_KEY, disabled: false, keyless: false });
});

it('flags keys that came from keyless mode', () => {
renderToStaticMarkup(
<ClerkProvider
publishableKey={DEV_KEY}
__internal_keyless_claimKeylessApplicationUrl='https://dashboard.clerk.com/claim'
>
child
</ClerkProvider>,
);

expect(notice).toHaveBeenCalledWith(expect.objectContaining({ keyless: true }));
});

it('passes the opt-out through when set as a prop', () => {
renderToStaticMarkup(
<ClerkProvider
publishableKey={DEV_KEY}
unsafe_disableDevelopmentModeConsoleWarning
>
child
</ClerkProvider>,
);

expect(notice).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
});
});
Loading
Loading