diff --git a/.changeset/canonical-dashboard-links.md b/.changeset/canonical-dashboard-links.md new file mode 100644 index 00000000000..c76a501f11e --- /dev/null +++ b/.changeset/canonical-dashboard-links.md @@ -0,0 +1,11 @@ +--- +'@clerk/backend': patch +'@clerk/chrome-extension': patch +'@clerk/expo': patch +'@clerk/express': patch +'@clerk/shared': patch +'@clerk/ui': patch +'@clerk/upgrade': patch +--- + +Update Clerk Dashboard links in option descriptions, error messages, and READMEs to use the active-instance shortcut (`https://dashboard.clerk.com/~/…`) instead of the legacy `/last-active?path=…` URL. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abe113d9e63..e612ce6e70e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,9 @@ jobs: - name: Check Formatting run: pnpm format:check + - name: Check Dashboard links + run: pnpm lint:dashboard-links + - name: Verify localizations are generated run: | pnpm --filter @clerk/localizations generate diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index 5457fb67505..f75845a384a 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -58,7 +58,7 @@ test.describe('Keyless mode @quickstart', () => { publishableKey, secretKey, claimUrl: 'https://dashboard.clerk.com/apps/claim', - apiKeysUrl: 'https://dashboard.clerk.com/last-active?path=api-keys', + apiKeysUrl: 'https://dashboard.clerk.com/~/api-keys', }); await app.keylessToEnv(); /** diff --git a/package.json b/package.json index 73b73b31d19..1d17281799d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "integration:secrets": "node ./scripts/1password-keys.mjs", "lint": "node ./scripts/lint.mjs", "lint:attw": "FORCE_COLOR=1 turbo lint:attw", + "lint:dashboard-links": "node ./scripts/check-dashboard-links.mjs", "lint:fix": "FORCE_COLOR=1 turbo lint -- --fix", "lint:inspect": "pnpx @eslint/config-inspector@latest", "lint:packages": "FORCE_COLOR=1 turbo lint", diff --git a/packages/backend/src/tokens/keys.ts b/packages/backend/src/tokens/keys.ts index 25043b920bc..4966bbfd7d7 100644 --- a/packages/backend/src/tokens/keys.ts +++ b/packages/backend/src/tokens/keys.ts @@ -109,7 +109,7 @@ export type LoadClerkJWKFromRemoteOptions = { */ skipJwksCache?: boolean; /** - * The Clerk Secret Key from the [**API keys**](https://dashboard.clerk.com/last-active?path=api-keys) page in the Clerk Dashboard. + * The Clerk Secret Key from the [**API keys**](https://dashboard.clerk.com/~/api-keys) page in the Clerk Dashboard. */ secretKey?: string; /** diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 823503a4aba..f54724da76b 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -18,7 +18,7 @@ import type { VerifyTokenOptions } from './verify'; */ export type AuthenticateRequestOptions = { /** - * The Clerk Publishable Key from the [**API keys**](https://dashboard.clerk.com/last-active?path=api-keys) page in the Clerk Dashboard. + * The Clerk Publishable Key from the [**API keys**](https://dashboard.clerk.com/~/api-keys) page in the Clerk Dashboard. */ publishableKey?: string; /** diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index f4aab370fc1..3921b6ca04c 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -35,7 +35,7 @@ export type VerifyTokenOptions = Simplify< Omit & Omit & { /** - * Used to verify the session token in a networkless manner. Supply the PEM public key from the **[**API keys**](https://dashboard.clerk.com/last-active?path=api-keys) page -> Show JWT public key -> PEM Public Key** section in the Clerk Dashboard. **It's recommended to use [the environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables) instead.** For more information, refer to [Manual JWT verification](https://clerk.com/docs/guides/sessions/manual-jwt-verification). + * Used to verify the session token in a networkless manner. Supply the PEM public key from the **[**API keys**](https://dashboard.clerk.com/~/api-keys) page -> Show JWT public key -> PEM Public Key** section in the Clerk Dashboard. **It's recommended to use [the environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables) instead.** For more information, refer to [Manual JWT verification](https://clerk.com/docs/guides/sessions/manual-jwt-verification). */ jwtKey?: string; } diff --git a/packages/chrome-extension/README.md b/packages/chrome-extension/README.md index 1e839f32069..3f759e1d65f 100644 --- a/packages/chrome-extension/README.md +++ b/packages/chrome-extension/README.md @@ -55,7 +55,7 @@ Please see the latest extension [authentication support matrix](https://clerk.co -d '{"allowed_origins": ["chrome-extension://"]}' ``` -4. **Set Environment Variables:** Retrieve the **Publishable key** from your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys&utm_source=github&utm_medium=clerk_chrome_extension) and set it as an environment variable. +4. **Set Environment Variables:** Retrieve the **Publishable key** from your [Clerk dashboard](https://dashboard.clerk.com/~/api-keys?utm_source=github&utm_medium=clerk_chrome_extension) and set it as an environment variable. ```sh # Vite diff --git a/packages/chrome-extension/docs/manifest.md b/packages/chrome-extension/docs/manifest.md index 88d83a94a2f..efd36e4533a 100644 --- a/packages/chrome-extension/docs/manifest.md +++ b/packages/chrome-extension/docs/manifest.md @@ -50,4 +50,4 @@ You must enable the following host permissions in your `manifest.json` file. Thi - Please make sure to include `/*` at the end of each `host_permission`. Feel free to later scope this down, if your usage sees fit. - The `YOUR_PRODUCTION_APP_DOMAIN` and `YOUR_CLERK_PRODUCTION_FRONTEND_API` are only required when you're ready to go to production. -Your Frontend API URLs can be found in [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=api-keys) under the **Show API URLs** option. +Your Frontend API URLs can be found in [Clerk Dashboard](https://dashboard.clerk.com/~/api-keys) under the **Show API URLs** option. diff --git a/packages/chrome-extension/src/react/ClerkProvider.tsx b/packages/chrome-extension/src/react/ClerkProvider.tsx index d26b940eff7..6c5abb44d94 100644 --- a/packages/chrome-extension/src/react/ClerkProvider.tsx +++ b/packages/chrome-extension/src/react/ClerkProvider.tsx @@ -9,7 +9,7 @@ import type { StorageCache } from '../internal/utils/storage'; type ChromeExtensionClerkProviderProps = Omit & { /** - * Your Clerk Publishable Key, available in the [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=api-keys). + * Your Clerk Publishable Key, available in the [Clerk Dashboard](https://dashboard.clerk.com/~/api-keys). * Required for Chrome Extensions, which cannot use `@clerk/react`'s environment-variable fallback. */ publishableKey: string; diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index e1630787a22..56230604852 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -311,7 +311,7 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { if (__DEV__ && !nativeApiErrorShown && response.payload?.errors?.[0]?.code === 'native_api_disabled') { console.error( - 'The Native API is disabled for this instance.\nGo to Clerk Dashboard > Configure > Native applications to enable it.\nOr, navigate here: https://dashboard.clerk.com/last-active?path=native-applications', + 'The Native API is disabled for this instance.\nGo to Clerk Dashboard > Configure > Native applications to enable it.\nOr, navigate here: https://dashboard.clerk.com/~/native-applications', ); nativeApiErrorShown = true; } diff --git a/packages/express/README.md b/packages/express/README.md index 37edf00f2f9..25d7dafe7dc 100644 --- a/packages/express/README.md +++ b/packages/express/README.md @@ -43,7 +43,7 @@ npm install @clerk/express ## Usage -Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=api-keys) and inside the **API Keys** section copy the publishable key and secret key. +Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/~/api-keys) and inside the **API Keys** section copy the publishable key and secret key. Paste your keys into an `.env` file: diff --git a/packages/shared/src/errors/errorThrower.ts b/packages/shared/src/errors/errorThrower.ts index 491a8e8c04e..9375e994eff 100644 --- a/packages/shared/src/errors/errorThrower.ts +++ b/packages/shared/src/errors/errorThrower.ts @@ -6,21 +6,21 @@ npx clerk@latest init \`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive. -If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy its Publishable key from https://dashboard.clerk.com/last-active?path=api-keys.`, +If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy its Publishable key from https://dashboard.clerk.com/~/api-keys.`, MissingPublishableKeyErrorMessage: `Missing publishableKey. To set up Clerk for this project, in your terminal run: npx clerk@latest init \`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive. -If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/last-active?path=api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, +If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/~/api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, MissingSecretKeyErrorMessage: `Missing secretKey. To set up Clerk for this project, in your terminal run: npx clerk@latest init \`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive. -If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/last-active?path=api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, +If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/~/api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, }); diff --git a/packages/shared/src/internal/clerk-js/warnings.ts b/packages/shared/src/internal/clerk-js/warnings.ts index 51ea3a6a4bc..7785a50d4b9 100644 --- a/packages/shared/src/internal/clerk-js/warnings.ts +++ b/packages/shared/src/internal/clerk-js/warnings.ts @@ -35,7 +35,7 @@ const createCannotRenderComponentWhenPermissionIsMissing = (componentName: 'Invi const createMessageForDisabledBilling = (componentName: 'PricingTable' | 'Checkout' | 'PlanDetails') => { return formatWarning( - `The <${componentName}/> component cannot be rendered when billing is disabled. Visit 'https://dashboard.clerk.com/last-active?path=billing/settings' to follow the necessary steps to enable billing. Since billing is disabled, this is no-op.`, + `The <${componentName}/> component cannot be rendered when billing is disabled. Visit 'https://dashboard.clerk.com/~/billing/settings' to follow the necessary steps to enable billing. Since billing is disabled, this is no-op.`, ); }; diff --git a/packages/shared/src/keys.ts b/packages/shared/src/keys.ts index c293a74bcb9..9949277f0da 100644 --- a/packages/shared/src/keys.ts +++ b/packages/shared/src/keys.ts @@ -104,7 +104,7 @@ npx clerk@latest init \`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive. -If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy them from https://dashboard.clerk.com/last-active?path=api-keys.`; +If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy them from https://dashboard.clerk.com/~/api-keys.`; export function parsePublishableKey( key: string | undefined, diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index bf5325b7e62..49f7875870c 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -3184,7 +3184,7 @@ export type IsomorphicClerkOptions = Without & { */ __internal_clerkUIVersion?: string; /** - * The Clerk Publishable Key for your instance. This can be found on the [API keys](https://dashboard.clerk.com/last-active?path=api-keys) page in the Clerk Dashboard. + * The Clerk Publishable Key for your instance. This can be found on the [API keys](https://dashboard.clerk.com/~/api-keys) page in the Clerk Dashboard. */ publishableKey: string; /** diff --git a/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx b/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx index 69ed7ae6bc1..1ad4b5c88c8 100644 --- a/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx +++ b/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx @@ -22,7 +22,7 @@ function withLastActiveFallback(callback: () => string): string { try { return callback(); } catch { - return 'https://dashboard.clerk.com/last-active'; + return 'https://dashboard.clerk.com/~'; } } diff --git a/packages/upgrade/src/guide-generators/core-2/retheme/intro.mdx b/packages/upgrade/src/guide-generators/core-2/retheme/intro.mdx index 844960a3f94..034fa064dfb 100644 --- a/packages/upgrade/src/guide-generators/core-2/retheme/intro.mdx +++ b/packages/upgrade/src/guide-generators/core-2/retheme/intro.mdx @@ -8,4 +8,4 @@ The new version ships with improved design and UX across all of Clerk's [UI comp The sections below contain more info on each change made to the customization ids and localization keys for reference. Regardless of how thoroughly you have reviewed the following information, we still recommend that you ensure that you have taken some time to manually look through each of your views to ensure that everything looks good still. -Also worth noting - if you are using Clerk's account portal (hosted sign in page), and would like to enable the new design there, you can do so via the "customization" tab on the [account portal page](https://dashboard.clerk.com/last-active?path=account-portal) in your dashboard. Selecting "Core 2" from the "ClerkJS Components" dropdown will enable the new designs. +Also worth noting - if you are using Clerk's account portal (hosted sign in page), and would like to enable the new design there, you can do so via the "customization" tab on the [account portal page](https://dashboard.clerk.com/~/account-portal) in your dashboard. Selecting "Core 2" from the "ClerkJS Components" dropdown will enable the new designs. diff --git a/packages/upgrade/src/versions/core-2/common/api-key-to-secret-key.md b/packages/upgrade/src/versions/core-2/common/api-key-to-secret-key.md index 6d2d0e1ba4c..83fcb774090 100644 --- a/packages/upgrade/src/versions/core-2/common/api-key-to-secret-key.md +++ b/packages/upgrade/src/versions/core-2/common/api-key-to-secret-key.md @@ -4,4 +4,4 @@ category: 'deprecation-removal' matcher: 'CLERK_API_KEY' --- -The `CLERK_API_KEY` environment variable was renamed to `CLERK_SECRET_KEY`. You can visit your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). +The `CLERK_API_KEY` environment variable was renamed to `CLERK_SECRET_KEY`. You can visit your [Clerk dashboard](https://dashboard.clerk.com/~/api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). diff --git a/packages/upgrade/src/versions/core-2/common/clerkprovider-frontendapi-2.md b/packages/upgrade/src/versions/core-2/common/clerkprovider-frontendapi-2.md index 33238534760..2212c398a2f 100644 --- a/packages/upgrade/src/versions/core-2/common/clerkprovider-frontendapi-2.md +++ b/packages/upgrade/src/versions/core-2/common/clerkprovider-frontendapi-2.md @@ -5,4 +5,4 @@ category: 'deprecation-removal' matcherFlags: 'm' --- -The `frontendApi` prop passed to `` was renamed to `publishableKey`. **Note:** The values are different, so this is not just a key replacement. You can visit your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). [More information](/docs/deployments/overview#api-keys-and-environment-variables). +The `frontendApi` prop passed to `` was renamed to `publishableKey`. **Note:** The values are different, so this is not just a key replacement. You can visit your [Clerk dashboard](https://dashboard.clerk.com/~/api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). [More information](/docs/deployments/overview#api-keys-and-environment-variables). diff --git a/packages/upgrade/src/versions/core-2/common/frontend-api-to-publishable-key.md b/packages/upgrade/src/versions/core-2/common/frontend-api-to-publishable-key.md index b1d8ba648d2..3bca074aa1c 100644 --- a/packages/upgrade/src/versions/core-2/common/frontend-api-to-publishable-key.md +++ b/packages/upgrade/src/versions/core-2/common/frontend-api-to-publishable-key.md @@ -4,4 +4,4 @@ category: 'deprecation-removal' matcher: 'CLERK_FRONTEND_API' --- -The `CLERK_FRONTEND_API` environment variable was renamed to `CLERK_PUBLISHABLE_KEY`. You can visit your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). **Note:** The values are different, so this is not just a key replacement. [More information](/docs/deployments/overview#api-keys-and-environment-variables). +The `CLERK_FRONTEND_API` environment variable was renamed to `CLERK_PUBLISHABLE_KEY`. You can visit your [Clerk dashboard](https://dashboard.clerk.com/~/api-keys) to copy/paste the new keys after choosing your framework. Make sure to update this in all environments (e.g. dev, staging, production). **Note:** The values are different, so this is not just a key replacement. [More information](/docs/deployments/overview#api-keys-and-environment-variables). diff --git a/scripts/check-dashboard-links.mjs b/scripts/check-dashboard-links.mjs new file mode 100644 index 00000000000..6032f9b6608 --- /dev/null +++ b/scripts/check-dashboard-links.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +// `/last-active?path=…` is a legacy Dashboard route that still resolves, so downstream route +// validation (clerk-docs) can't tell it apart from a broken link. Ban it at the source instead. +const DASHBOARD_ORIGIN = 'https://dashboard.clerk.com'; +const DASHBOARD_URL_PATTERN = /https:\/\/dashboard\.clerk\.com[^\s<>"'`)\]}*\\]*/g; +const LEGACY_PATH = '/last-active'; +const IGNORED_FILES = new Set(['scripts/check-dashboard-links.mjs', 'scripts/check-dashboard-links.test.mjs']); + +function trimUrl(url) { + return url.replace(/[),.;:]+$/, ''); +} + +function isLegacyDashboardUrl(rawUrl) { + let url; + try { + url = new URL(rawUrl); + } catch { + return false; + } + return url.origin === DASHBOARD_ORIGIN && (url.pathname === LEGACY_PATH || url.pathname === `${LEGACY_PATH}/`); +} + +export function isIgnoredFile(file) { + return IGNORED_FILES.has(file) || file.startsWith('.changeset/') || /(^|\/)CHANGELOG\.md$/.test(file); +} + +export function findLegacyDashboardLinks(file, content) { + const findings = []; + const lines = content.split('\n'); + + lines.forEach((text, index) => { + for (const match of text.matchAll(DASHBOARD_URL_PATTERN)) { + const url = trimUrl(match[0]); + if (isLegacyDashboardUrl(url)) { + findings.push({ file, line: index + 1, column: match.index + 1, url }); + } + } + }); + + return findings; +} + +export function formatLegacyLinkError({ file, line, column, url }) { + const parsed = new URL(url); + const path = parsed.searchParams.get('path'); + parsed.searchParams.delete('path'); + parsed.pathname = path ? `/~/${path.replace(/^\/+/, '')}` : '/~'; + + return `${file}:${line}:${column} ${url} → ${parsed.toString()}`; +} + +async function main() { + const files = execFileSync('git', ['ls-files', '-z'], { encoding: 'utf8' }) + .split('\0') + .filter(file => file && !isIgnoredFile(file)); + + const findings = []; + for (const file of files) { + let content; + try { + content = await readFile(file, 'utf8'); + } catch { + continue; + } + if (!content.includes(`${DASHBOARD_ORIGIN}${LEGACY_PATH}`)) { + continue; + } + findings.push(...findLegacyDashboardLinks(file, content)); + } + + if (findings.length === 0) { + console.log('No legacy Dashboard links found.'); + return; + } + + console.error(`Found ${findings.length} legacy Dashboard link(s). Use the active-instance shortcut instead:\n`); + for (const finding of findings) { + console.error(` ${formatLegacyLinkError(finding)}`); + } + process.exit(1); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await main(); +} diff --git a/scripts/check-dashboard-links.test.mjs b/scripts/check-dashboard-links.test.mjs new file mode 100644 index 00000000000..9f7c2fd85ab --- /dev/null +++ b/scripts/check-dashboard-links.test.mjs @@ -0,0 +1,108 @@ +import { describe, expect, test } from 'vitest'; + +import { findLegacyDashboardLinks, formatLegacyLinkError, isIgnoredFile } from './check-dashboard-links.mjs'; + +describe('findLegacyDashboardLinks', () => { + test('flags /last-active links with a path query', () => { + const content = 'Copy them from https://dashboard.clerk.com/last-active?path=api-keys.'; + + expect(findLegacyDashboardLinks('a.ts', content)).toEqual([ + { file: 'a.ts', line: 1, column: 16, url: 'https://dashboard.clerk.com/last-active?path=api-keys' }, + ]); + }); + + test('flags a bare /last-active link', () => { + const content = "return 'https://dashboard.clerk.com/last-active';"; + + expect(findLegacyDashboardLinks('a.ts', content)).toMatchObject([ + { url: 'https://dashboard.clerk.com/last-active' }, + ]); + }); + + test('reports the line of each occurrence', () => { + const content = [ + 'ok https://dashboard.clerk.com/~/api-keys', + '', + 'bad [link](https://dashboard.clerk.com/last-active?path=billing/settings)', + ].join('\n'); + + expect(findLegacyDashboardLinks('a.md', content)).toMatchObject([ + { line: 3, url: 'https://dashboard.clerk.com/last-active?path=billing/settings' }, + ]); + }); + + test('accepts canonical /~/ links and non-instance Dashboard URLs', () => { + const content = [ + 'https://dashboard.clerk.com/~/api-keys', + 'https://dashboard.clerk.com/~', + 'https://dashboard.clerk.com/apps/claim', + 'https://dashboard.clerk.com', + ].join('\n'); + + expect(findLegacyDashboardLinks('a.ts', content)).toEqual([]); + }); + + test('ignores look-alike hosts and paths that merely contain the segment', () => { + const content = [ + 'https://dashboard.clerk.com.evil/last-active', + 'https://dashboard.clerk.com/~/last-active-thing', + 'https://clerk.com/docs/last-active', + ].join('\n'); + + expect(findLegacyDashboardLinks('a.ts', content)).toEqual([]); + }); +}); + +describe('isIgnoredFile', () => { + test('skips changelogs, changesets, and the checker itself', () => { + expect(isIgnoredFile('packages/clerk-js/CHANGELOG.md')).toBe(true); + expect(isIgnoredFile('.changeset/fix-links.md')).toBe(true); + expect(isIgnoredFile('scripts/check-dashboard-links.mjs')).toBe(true); + expect(isIgnoredFile('scripts/check-dashboard-links.test.mjs')).toBe(true); + }); + + test('checks source, docs, and READMEs', () => { + expect(isIgnoredFile('packages/shared/src/keys.ts')).toBe(false); + expect(isIgnoredFile('packages/express/README.md')).toBe(false); + expect(isIgnoredFile('integration/tests/keyless.test.ts')).toBe(false); + }); +}); + +describe('formatLegacyLinkError', () => { + test('points at the canonical replacement', () => { + const message = formatLegacyLinkError({ + file: 'a.ts', + line: 3, + column: 5, + url: 'https://dashboard.clerk.com/last-active?path=api-keys', + }); + + expect(message).toBe( + 'a.ts:3:5 https://dashboard.clerk.com/last-active?path=api-keys → https://dashboard.clerk.com/~/api-keys', + ); + }); + + test('maps a bare /last-active to /~', () => { + const message = formatLegacyLinkError({ + file: 'a.ts', + line: 1, + column: 1, + url: 'https://dashboard.clerk.com/last-active', + }); + + expect(message).toBe('a.ts:1:1 https://dashboard.clerk.com/last-active → https://dashboard.clerk.com/~'); + }); + + test('keeps other query params', () => { + const message = formatLegacyLinkError({ + file: 'a.ts', + line: 1, + column: 1, + url: 'https://dashboard.clerk.com/last-active?path=api-keys&utm_source=github', + }); + + expect(message).toBe( + 'a.ts:1:1 https://dashboard.clerk.com/last-active?path=api-keys&utm_source=github → https://dashboard.clerk.com/~/api-keys?utm_source=github', + ); + }); +}); diff --git a/scripts/lint.mjs b/scripts/lint.mjs index 04af53e7dd2..c2441d8f3f5 100644 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -9,10 +9,11 @@ $.env = { $.stdio = 'inherit'; $.verbose = !!process.env.VERBOSE; -const [github, integration, scripts] = await Promise.allSettled([ +const [github, integration, scripts, dashboardLinks] = await Promise.allSettled([ $`echo "SKIPPING: pnpm eslint .github/workflows"`, $`pnpm eslint integration`, $`pnpm eslint scripts`, + $`pnpm lint:dashboard-links`, ]); let packages; @@ -37,6 +38,7 @@ function logLintResult(status, directory, command) { logLintResult(github.status, 'GitHub Actions', 'pnpm eslint .github/workflows'); logLintResult(integration.status, 'Integration directory', 'pnpm eslint integration'); logLintResult(scripts.status, 'Scripts directory', 'pnpm eslint scripts'); +logLintResult(dashboardLinks.status, 'Dashboard links', 'pnpm lint:dashboard-links'); if (packages?.exitCode) { console.log(''); @@ -44,6 +46,6 @@ if (packages?.exitCode) { console.log(`Run '${chalk.bold('pnpm turbo lint')}' to reproduce the error`); } -if (integration.status === 'rejected' || packages?.exitCode) { +if (integration.status === 'rejected' || dashboardLinks.status === 'rejected' || packages?.exitCode) { process.exit(1); }