From 4fd7d1b3a8478b4ca14a9966cf08032224952b31 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 17 Jul 2025 14:12:53 -0400 Subject: [PATCH 001/136] bring in kyles scripts --- .gitignore | 1 + package.json | 4 +- proposal-mapping.json | 638 ++++++++++++++++++ proposal.md | 134 ++++ .../migration-assistant/generate-manifest.ts | 271 ++++++++ scripts/migration-assistant/map-content.ts | 438 ++++++++++++ 6 files changed, 1485 insertions(+), 1 deletion(-) create mode 100644 proposal-mapping.json create mode 100644 proposal.md create mode 100755 scripts/migration-assistant/generate-manifest.ts create mode 100644 scripts/migration-assistant/map-content.ts diff --git a/.gitignore b/.gitignore index 73ec6f8efc..b7ca1e30b9 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ yarn.lock /docs/docs /docs/docs/ +public/manifest.proposal.json diff --git a/package.json b/package.json index 4f4dad6256..16e13dee48 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "dev": "tsx watch ./scripts/build-docs.ts --watch", "test": "vitest --silent", "typedoc:link": "node scripts/link-typedoc.mjs", - "move-doc": "node scripts/move-doc.mjs" + "move-doc": "node scripts/move-doc.mjs", + "migration:generate-manifest": "tsx scripts/migration-assistant/generate-manifest.ts", + "migration:map-content": "tsx scripts/migration-assistant/map-content.ts" }, "devDependencies": { "@parcel/watcher": "^2.5.1", diff --git a/proposal-mapping.json b/proposal-mapping.json new file mode 100644 index 0000000000..c8b734f28d --- /dev/null +++ b/proposal-mapping.json @@ -0,0 +1,638 @@ +{ + "how-clerk-works/overview.mdx": { + "newPath": "/docs/core-concepts/how-clerk-works", + "action": "consolidate" + }, + "how-clerk-works/cookies.mdx": { + "newPath": "/docs/core-concepts/how-clerk-works", + "action": "consolidate" + }, + "security/overview.mdx": { + "newPath": "/docs/core-concepts/security", + "action": "move" + }, + "backend-requests/custom-session-token.mdx": { + "newPath": "/docs/guides/configure/session-token-customization", + "action": "move" + }, + "backend-requests/jwt-templates.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/making-requests.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/manual-jwt.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/overview.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/resources/cookies.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/resources/rate-limits.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/resources/session-tokens.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "backend-requests/resources/tokens-and-signatures.mdx": { + "newPath": "/docs/guides/configure/backend-requests", + "action": "consolidate" + }, + "webhooks/debug-your-webhooks.mdx": { + "newPath": "/docs/guides/configure/syncing-data-with-webhooks", + "action": "consolidate" + }, + "webhooks/inngest.mdx": { + "newPath": "/docs/guides/configure/syncing-data-with-webhooks", + "action": "consolidate" + }, + "webhooks/loops.mdx": { + "newPath": "/docs/guides/configure/syncing-data-with-webhooks", + "action": "consolidate" + }, + "webhooks/overview.mdx": { + "newPath": "/docs/guides/configure/syncing-data-with-webhooks", + "action": "consolidate" + }, + "webhooks/sync-data.mdx": { + "newPath": "/docs/guides/configure/syncing-data-with-webhooks", + "action": "consolidate" + }, + "integrations/analytics/google-analytics.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/convex.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/fauna.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/firebase.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/grafbase.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/hasura.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/instantdb.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/neon.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/nhost.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/databases/supabase.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/overview.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/shopify.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "integrations/vercel-marketplace.mdx": { + "newPath": "/docs/guides/configure/integrations", + "action": "consolidate" + }, + "ai-prompts/nextjs.mdx": { + "newPath": "/docs/core-concepts/ai-prompts", + "action": "consolidate" + }, + "ai-prompts/overview.mdx": { + "newPath": "/docs/core-concepts/ai-prompts", + "action": "consolidate" + }, + "ai-prompts/react.mdx": { + "newPath": "/docs/core-concepts/ai-prompts", + "action": "consolidate" + }, + "security/bot-protection.mdx": { + "newPath": "/docs/guides/secure/bot-detection", + "action": "move" + }, + "components/**": { + "newPath": "/docs/reference/components/**", + "action": "generate" + }, + "hooks/**": { + "newPath": "/docs/reference/hooks/**", + "action": "generate" + }, + "references/sdk/**": { + "newPath": "/docs/guides/development/sdk-development", + "action": "consolidate" + }, + "references/**": { + "newPath": "/docs/reference/sdk/**", + "action": "generate" + }, + "billing/b2b-saas.mdx": { + "newPath": "/docs/guides/billing/for-b2b", + "action": "move" + }, + "billing/b2c-saas.mdx": { + "newPath": "/docs/guides/billing/for-b2c", + "action": "move" + }, + "billing/overview.mdx": { + "newPath": "/docs/guides/billing/overview", + "action": "move" + }, + "/docs/examples/testing/test-link": { + "newPath": null, + "action": "drop" + }, + "deployments/migrate-from-cognito.mdx": { + "newPath": "/docs/guides/development/migrating-your-data", + "action": "move" + }, + "deployments/migrate-from-firebase.mdx": { + "newPath": "/docs/guides/development/migrating-your-data", + "action": "move" + }, + "deployments/migrate-overview.mdx": { + "newPath": "/docs/guides/development/migrating-your-data", + "action": "move" + }, + "deployments/clerk-environment-variables.mdx": { + "newPath": "/docs/guides/development/clerk-environment-variables", + "action": "move" + }, + "deployments/**": { + "newPath": "/docs/guides/development/deployment", + "action": "consolidate" + }, + "custom-flows/accept-organization-invitations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/add-email.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/add-phone.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/application-invitations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/bot-sign-up-protection.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/create-organizations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/email-links.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/email-password-mfa.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/email-password.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/email-sms-otp.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/embedded-email-links.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/enterprise-connections.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/error-handling.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/forgot-password.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/google-one-tap.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-membership-requests.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-organization-invitations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-roles.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-sms-based-mfa.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-sso-connections.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-totp-based-mfa.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/manage-user-org-invitations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/multi-session-applications.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/oauth-connections.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/organization-switcher.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/overview.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/passkeys.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/sign-out.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/update-organizations.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "custom-flows/user-impersonation.mdx": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "versioning/available-versions.mdx": { + "newPath": "/docs/guides/development/upgrading/versioning", + "action": "consolidate" + }, + "versioning/overview.mdx": { + "newPath": "/docs/guides/development/upgrading/versioning", + "action": "consolidate" + }, + "errors/overview.mdx": { + "newPath": "/docs/guides/development/troubleshooting", + "action": "move" + }, + "authentication/web3/**": { + "newPath": "/docs/guides/configure/auth-strategies/web3", + "action": "consolidate" + }, + "authentication/social-connections/**": { + "newPath": "/docs/guides/configure/social-connections/**", + "action": "generate" + }, + "organizations/roles-permissions.mdx": { + "newPath": "/docs/guides/b2b/roles-and-permissions", + "action": "consolidate" + }, + "organizations/verified-domains.mdx": { + "newPath": "/docs/guides/b2b/verified-domains", + "action": "move" + }, + "security/clerk-csp.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/csrf-protection.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/email-link-protection.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/fixation-protection.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/password-protection.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/unauthorized-sign-in.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/user-lock-guide.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/vulnerability-disclosure-policy.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "security/xss-leak-protection.mdx": { + "newPath": "/docs/guides/secure/best-practices", + "action": "consolidate" + }, + "testing/cypress/custom-commands.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/cypress/overview.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/cypress/test-account-portal.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/overview.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/playwright/overview.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/playwright/test-authenticated-flows.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/playwright/test-helpers.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/postman-or-insomnia.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "testing/test-emails-and-phones.mdx": { + "newPath": "/docs/guides/development/testing-with-clerk", + "action": "consolidate" + }, + "authentication/enterprise-connections/account-linking.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/authentication-flows.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/easie/google.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/easie/microsoft.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/jit-provisioning.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/oidc/custom-provider.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/overview.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/saml/azure.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/saml/custom-provider.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/saml/google.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "authentication/enterprise-connections/saml/okta.mdx": { + "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", + "action": "consolidate" + }, + "troubleshooting/create-a-minimal-reproduction.mdx": { + "newPath": "/docs/guides/development/troubleshooting", + "action": "consolidate" + }, + "troubleshooting/email-deliverability.mdx": { + "newPath": "/docs/guides/development/troubleshooting", + "action": "consolidate" + }, + "troubleshooting/overview.mdx": { + "newPath": "/docs/guides/development/troubleshooting", + "action": "consolidate" + }, + "troubleshooting/script-loading.mdx": { + "newPath": "/docs/guides/development/troubleshooting", + "action": "consolidate" + }, + "upgrade-guides/long-term-support.mdx": { + "newPath": "/docs/guides/development/upgrading/versioning", + "action": "consolidate" + }, + "upgrade-guides/sdk-versioning.mdx": { + "newPath": "/docs/guides/development/upgrading/versioning", + "action": "consolidate" + }, + "customization/overview.mdx": { + "newPath": "/docs/guides/customizing-clerk/appearance-prop", + "action": "consolidate" + }, + "customization/themes.mdx": { + "newPath": "/docs/guides/customizing-clerk/appearance-prop", + "action": "consolidate" + }, + "customization/user-button.mdx": { + "newPath": "/docs/guides/customizing-clerk/appearance-prop", + "action": "consolidate" + }, + "customization/user-profile.mdx": { + "newPath": "/docs/guides/customizing-clerk/appearance-prop", + "action": "consolidate" + }, + "customization/variables.mdx": { + "newPath": "/docs/guides/customizing-clerk/appearance-prop", + "action": "consolidate" + }, + "organizations/overview.mdx": { + "newPath": "/docs/guides/b2b/overview", + "action": "move" + }, + "organizations/manage-sso.mdx": { + "newPath": "/docs/guides/b2b/managing-orgs", + "action": "consolidate" + }, + "organizations/invitations.mdx": { + "newPath": "/docs/guides/b2b/managing-orgs", + "action": "consolidate" + }, + "organizations/create-roles-permissions.mdx": { + "newPath": "/docs/guides/b2b/roles-and-permissions", + "action": "consolidate" + }, + "organizations/creator-role.mdx": { + "newPath": "/docs/guides/b2b/roles-and-permissions", + "action": "consolidate" + }, + "organizations/default-role.mdx": { + "newPath": "/docs/guides/b2b/roles-and-permissions", + "action": "consolidate" + }, + "quickstarts/**": { + "newPath": "/docs/getting-started/quickstart/**", + "action": "generate" + }, + "upgrade-guides/expo/v2.mdx": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/expo-v2", + "action": "move" + }, + "upgrade-guides/node-to-express.mdx": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/node-to-express", + "action": "move" + }, + "upgrade-guides/url-based-session-syncing.mdx": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/url-session-syncing", + "action": "move" + }, + "upgrade-guides/core-2/**": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/core-2", + "action": "consolidate" + }, + "customization/elements/**": { + "newPath": "/docs/examples", + "action": "convert-to-example" + }, + "customization/elements/overview.mdx": { + "newPath": "/docs/guides/customizing-clerk/elements", + "action": "move" + }, + "customization/elements/reference/**": { + "newPath": "/docs/reference/components/elements/**", + "action": "generate" + }, + "customization/organization-profile.mdx": { + "newPath": "/docs/guides/customizing-clerk/adding-items", + "action": "move" + }, + "customization/localization.mdx": { + "newPath": "/docs/guides/customizing-clerk/localization", + "action": "move" + }, + "upgrade-guides/progressive-sign-up.mdx": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/progressive-sign-ups", + "action": "move" + }, + "account-portal/**": { + "newPath": "/docs/guides/customizing-clerk/account-portal", + "action": "consolidate" + }, + "authentication/configuration/legal-compliance.mdx": { + "newPath": "/docs/guides/secure/legal-compliance", + "action": "move" + }, + "authentication/configuration/restrictions.mdx": { + "newPath": "/docs/guides/secure/restricting-access", + "action": "move" + }, + "users/user-impersonation.mdx": { + "newPath": "/docs/guides/users/impersonation", + "action": "move" + }, + "users/creating-users.mdx": { + "newPath": "/docs/guides/users/managing", + "action": "consolidate" + }, + "users/deleting-users.mdx": { + "newPath": "/docs/guides/users/managing", + "action": "consolidate" + }, + "users/invitations.mdx": { + "newPath": "/docs/guides/users/managing", + "action": "consolidate" + }, + "users/metadata.mdx": { + "newPath": "/docs/guides/users/managing", + "action": "consolidate" + }, + "users/overview.mdx": { + "newPath": "/docs/guides/users/managing", + "action": "consolidate" + }, + "upgrade-guides/nextjs/v6.mdx": { + "newPath": "/docs/guides/development/upgrading/upgrade-guides/next-v6", + "action": "move" + }, + "authentication/configuration/email-sms-templates.mdx": { + "newPath": "/docs/guides/customizing-clerk/email-and-sms-templates", + "action": "move" + }, + "advanced-usage/satellite-domains.mdx": { + "newPath": "/docs/guides/clerk-dashboard/dns-domains", + "action": "consolidate" + }, + "upgrade-guides/overview.mdx": { + "newPath": "/docs/guides/development/upgrading", + "action": "move" + }, + "guides/reverification.mdx": { + "newPath": "/docs/guides/secure/re-verification-step-up", + "action": "move" + }, + "advanced-usage/using-proxies.mdx": { + "newPath": "/docs/guides/clerk-dashboard/dns-domains", + "action": "consolidate" + }, + "authentication/configuration/session-options.mdx": { + "newPath": "/docs/guides/secure/session-options", + "action": "move" + }, + "guides/multi-tenant-architecture.mdx": { + "newPath": "/docs/core-concepts/multi-tenant-architecture", + "action": "move" + }, + "authentication/configuration/force-mfa.mdx": { + "newPath": "/docs/guides/secure/mfa", + "action": "consolidate" + } +} diff --git a/proposal.md b/proposal.md new file mode 100644 index 0000000000..2db5ce23cf --- /dev/null +++ b/proposal.md @@ -0,0 +1,134 @@ +# Top Level Links + +- Home +- Getting Started +- Guides +- Examples +- Reference + +## Home + +### Core Concepts + +- How Clerk Works +- Integrate Clerk +- Clerk Objects +- Security at Clerk [security] +- System Limits +- AI Prompt Library [ai-prompts] +- Multi-tenant Architecture + +## Getting Started [getting-started] + +### Quickstart + +- App Router +- Pages Router + +### Clerk Dashboard + +- Setting up your Clerk account +- Configure your application + +### Next Steps + +- Custom sign-in/up pages +- Protect specific routes +- Read user and session data +- Add middleware + +## Guides [guides] + +### Securing your App [secure] + +- Restricting Access +- Multifactor Authentication (MFA) [mfa] +- Bot Detection +- Banning Users +- Prevent brute force attacks +- Re-verification (Step-up) +- Legal Compliance +- Security Best Practices [best-practices] +- Session Options + +### Managing Users [users] + +- Managing Users [managing] +- Reading Clerk Data [reading] +- Extending Clerk Data [extending] +- Syncing Clerk Data [syncing] +- Impersonation + +### B2B + +- Overview +- Managing Organizations [managing-orgs] +- Verified Domains +- Roles and Permissions +- SSO / Enterprise Connections + +### Billing + +- Overview +- Billing for B2C [for-b2c] +- Billing for B2B [for-b2b] + +### Customizing Clerk + +- UI Customization (Appearance Prop) [appearance-prop] +- Account Portal +- Adding items to UI Components [adding-items] +- Email and SMS Templates +- Localization (i18n) [localization] +- Clerk Elements (beta) [elements] + +### Configuring your App [configure] + +- Authentication Strategies [auth-strategies] + - Social Connections + - Enterprise Connections + - Web3 +- Session Token customization +- Syncing data with webhooks +- Backend Requests +- Integrations + +### Clerk Dashboard + +- Overview +- Account Portal +- DNS & Domains +- Plans & Billing + +### Development + +- Deployment +- Testing with Clerk +- Managing Environments +- Migrating your Data +- Clerk environment variables +- SDK Development +- Upgrading Clerk [upgrading] + - Versioning & LTS [versioning] + - Upgrade Guides + - Core 2 + - Node to Express + - Expo v2 [expo-v2] + - @clerk/nextjs v6 [next-v6] + - URL based session syncing [url-session-syncing] + - Progressive Sign Ups +- Troubleshooting + +## Examples [examples] + +### Testing + +- Test Link + +## Reference [reference] + +### API Reference [reference] + +- SDK Reference [sdk] +- UI Components [components] +- API Reference diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts new file mode 100755 index 0000000000..2131626df0 --- /dev/null +++ b/scripts/migration-assistant/generate-manifest.ts @@ -0,0 +1,271 @@ +import fs from 'fs/promises' +import path from 'path' +import { z } from 'zod' + +const PROPOSAL_PATH = path.join(process.cwd(), './proposal.md') +const OUTPUT_PATH = path.join(process.cwd(), './public/manifest.proposal.json') + +// Convert to lowercase and replace spaces with hyphens +const slugify = (str: string) => + str + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + +/** + * Parse markdown content and convert to a single navigation structure + * @param {string} content - The markdown content + * @returns {Object} - Object containing navigation structure + */ +function parseMarkdownToManifest(content: string) { + const lines = content.split('\n') + const navigation: any[] = [] + let currentTopLevel: { title: string; items: any[] } | null = null + let currentTopLevelCustomSlug: string | null = null + let currentItemGroup: any[] = [] + let pathStack: string[] = [] // Stack to track nested path components + + const finishCurrentItemGroup = () => { + if (currentItemGroup && currentItemGroup.length > 0) { + if (currentTopLevel) { + currentTopLevel.items.push([...currentItemGroup]) + } + currentItemGroup = [] + } + } + + const finishCurrentTopLevel = () => { + finishCurrentItemGroup() + if (currentTopLevel) { + navigation.push([currentTopLevel]) + currentTopLevel = null + } + currentTopLevelCustomSlug = null + pathStack = [] + } + + for (const line of lines) { + const trimmed = line.trim() + + // Skip empty lines + if (!trimmed) continue + + // Skip the "Top Level Links" section and its content + if (trimmed === '# Top Level Links') { + continue + } + + // Skip top level sections (## Header) - we'll process their subsections + if (trimmed.startsWith('## ')) { + continue + } + + // Subsections (### Header) - these become top-level sections + if (trimmed.startsWith('### ')) { + finishCurrentTopLevel() + const titleWithBrackets = trimmed.substring(4) + const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) + + let title: string, customSlug: string | null + if (bracketMatch) { + title = bracketMatch[1].trim() + customSlug = bracketMatch[2].trim() + } else { + title = titleWithBrackets.trim() + customSlug = null + } + + currentTopLevel = { + title, + items: [], + } + currentTopLevelCustomSlug = customSlug + currentItemGroup = [] + pathStack = [] + continue + } + + // List items (- Item) - handle indentation levels + if (trimmed.startsWith('- ')) { + // Calculate indentation level (number of leading spaces before the -) + const leadingSpaces = line.length - line.trimStart().length + const indentLevel = Math.floor(leadingSpaces / 2) // Assuming 2 spaces per indent level + + const titleWithBrackets = trimmed.substring(2) + // Parse bracket syntax for list items + const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) + + let title: string, customSlug: string | null + if (bracketMatch) { + title = bracketMatch[1].trim() + customSlug = bracketMatch[2].trim() + } else { + title = titleWithBrackets.trim() + customSlug = null + } + + // Update pathStack based on indentation level + // Keep only the path components for levels above the current one + pathStack = pathStack.slice(0, indentLevel) + + // Add current item to pathStack + const itemSlug = customSlug || slugify(title) + pathStack.push(itemSlug) + + // Create the content item + const linkItem = { + title, + href: generateHref( + title, + currentTopLevel?.title, + undefined, + undefined, + itemSlug, + currentTopLevelCustomSlug || undefined, + ), + } + + // Initialize item group if needed + if (!currentItemGroup) { + currentItemGroup = [] + } + + // Add item to current group + currentItemGroup.push(linkItem) + } + } + + // Finish any remaining content + finishCurrentTopLevel() + + return { + navigation, + } +} + +/** + * Generate href from title and context + * @param {string} title - The item title + * @param {string} topLevel - The section title (formerly subsection) + * @param {string} section - Unused (kept for backward compatibility) + * @param {string} manifestKey - The manifest key for separate manifests + * @param {string} customSlug - The custom slug for the item + * @param {string} topLevelCustomSlug - The custom slug for the top-level section + * @returns {string} - The generated href + */ +function generateHref( + title: string, + topLevel?: string, + section?: string, + manifestKey?: string, + customSlug?: string, + topLevelCustomSlug?: string, +) { + let href = '/docs' + + // If this is a separate manifest, use the manifest key as the base path + if (manifestKey) { + href += `/${manifestKey}` + } + + // Add the section path (what used to be subsection, now top-level) + if (topLevel) { + // Use custom slug if provided, otherwise slugify the title + const sectionSlug = topLevelCustomSlug || slugify(topLevel) + href += `/${sectionSlug}` + } + + // Use custom slug for the item if provided, otherwise slugify the title + const itemSlug = customSlug || slugify(title) + href += `/${itemSlug}` + + return href +} + +export type ManifestItem = { + title: string + href: string +} + +export type ManifestGroup = { + title: string + items: Manifest +} + +export type Manifest = (ManifestItem | ManifestGroup)[][] + +const manifestItem: z.ZodType = z + .object({ + title: z.string(), + href: z.string(), + }) + .strict() + +const manifestGroup: z.ZodType = z + .object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + }) + .strict() + +const manifestSchema: z.ZodType = z.array(z.array(z.union([manifestItem, manifestGroup]))) + +/** + * Validate the manifest against the schema + * @param {Object} manifest - The manifest to validate + * @returns {boolean} - Whether the manifest is valid + */ +function validateManifest(manifest: object) { + const result = z.object({ navigation: manifestSchema }).safeParse(manifest) + + if (!result.success) { + console.error('Validation errors:') + console.error(result.error.errors) + return false + } + + return true +} + +/** + * Main function to generate the manifest + */ +async function generateManifest() { + console.log('πŸš€ Starting manifest generation...') + + // Read the ia-proposal.md file + const proposalContent = await fs.readFile(PROPOSAL_PATH, 'utf-8') + console.log('βœ… Read ia-proposal.md') + + // Parse markdown to multiple navigation structures + const manifest = parseMarkdownToManifest(proposalContent) + console.log('βœ… Parsed markdown structure') + + // Validate manifest against schema + if (!validateManifest(manifest)) { + console.error('❌ Manifest validation failed') + process.exit(1) + } + console.log('βœ… Main manifest validation passed') + + // Write the main manifest.new.json file + await fs.writeFile(OUTPUT_PATH, JSON.stringify(manifest)) + console.log('βœ… Written manifest.proposal.json') +} + +async function watchAndRebuild() { + const { argv } = process + + if (argv.includes('--watch')) { + const watcher = fs.watch(PROPOSAL_PATH) + for await (const event of watcher) { + generateManifest() + } + } +} + +// Run the bootstrap if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + generateManifest() + watchAndRebuild() +} diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts new file mode 100644 index 0000000000..cb4c185971 --- /dev/null +++ b/scripts/migration-assistant/map-content.ts @@ -0,0 +1,438 @@ +import { readFile } from 'fs/promises' +import { glob } from 'glob' +import { join, relative } from 'path' +import type { Manifest } from './generate-manifest' + +// Path to the docs directory and mapping file +const MAPPING_PATH = join(process.cwd(), './proposal-mapping.json') +const MANIFEST_PROPOSAL_PATH = join(process.cwd(), './public/manifest.proposal.json') +const MANIFEST_PATH = join(process.cwd(), './public/manifest.json') +const DOCS_PATH = join(process.cwd(), './docs') + +// Environment variable to control output +const WARNINGS_ONLY = process.env.DOCS_IA_WARNINGS_ONLY === 'true' + +/** + * Read and parse the mapping.json file + * @returns {Promise} Parsed mapping object + */ +async function readMapping() { + try { + const mappingContent = await readFile(MAPPING_PATH, 'utf-8') + return JSON.parse(mappingContent) as Record< + string, + { newPath: string; action: 'consolidate' | 'move' | 'generate' | 'convert-to-example' | 'drop' } + > + } catch (error) { + console.error(`❌ Error reading mapping.json: ${error.message}`) + return {} + } +} + +type Mapping = Awaited> + +/** + * Read all manifest files and extract destination paths + * @returns {Promise} Array of all destination paths from manifests + */ +async function readProposalManifestPaths() { + const content = await readFile(MANIFEST_PROPOSAL_PATH, 'utf-8') + const manifest = JSON.parse(content) as { navigation: Manifest } + return { manifest, paths: parseManifestPaths(manifest) } +} + +async function getManifestPaths() { + const content = await readFile(MANIFEST_PATH, 'utf-8') + const manifest = JSON.parse(content) as { navigation: Manifest } + return { manifest, paths: parseManifestPaths(manifest) } +} + +function parseManifestPaths(manifest: { navigation: Manifest }) { + const manifestPaths: string[] = [] + + // Extract paths from navigation structure + const extractPaths = (groups: Manifest) => { + for (const group of groups) { + for (const item of group) { + if ('href' in item) { + manifestPaths.push(item.href) + } + + if ('items' in item) { + extractPaths(item.items) + } + } + } + } + + extractPaths(manifest.navigation) + + return [...new Set(manifestPaths)].sort() // Remove duplicates and sort +} + +/** + * Recursively find all .mdx files in a directory + * @param {string} dir - Directory to search + * @param {string} baseDir - Base directory for relative paths + * @returns {Promise} Array of relative file paths + */ +async function findMdxFiles(dir: string, baseDir = dir) { + const files = await glob(`${dir}/**/*.mdx`, { ignore: ['**/_*/**'] }) + return files.map((file) => relative(baseDir, file)) +} + +/** + * Find unhandled files by comparing with mapping + * @param {string[]} allFiles - All MDX files found + * @param {Object} mapping - Mapping configuration + * @returns {string[]} Array of unhandled file paths + */ +function findUnhandledFiles(allFiles: string[], mapping: Mapping) { + const handledFiles = new Set(Object.keys(mapping)) + return allFiles.filter((file) => !handledFiles.has(file)) +} + +/** + * Find pages that need to be created (in manifest but no mapping source) + * @param manifestPaths - All paths from manifest files + * @param mapping - Mapping configuration + * @returns Array of paths that need new content + */ +function findPagesToCreate(manifestPaths: string[], mapping: Mapping) { + const mappedDestinations = new Set(Object.values(mapping).map((entry) => entry.newPath)) + + // Also collect manifest paths that are explicitly marked for dropping + const droppedPaths = new Set() + for (const [key, value] of Object.entries(mapping)) { + if (value.action === 'drop' && key.startsWith('/docs/')) { + droppedPaths.add(key) + } + } + + return manifestPaths.filter((path) => !mappedDestinations.has(path) && !droppedPaths.has(path)) +} + +/** + * Display warnings for unhandled files and pages to create + * @param {string[]} unhandledFiles - Array of unhandled file paths + * @param {string[]} pagesToCreate - Array of paths that need new content + * @param {Object} expandedMapping - Expanded mapping configuration + * @param {Array} invalidMappings - Array of invalid mapping objects + */ +function displayWarnings( + unhandledFiles: string[], + pagesToCreate: string[], + expandedMapping: Mapping, + invalidMappings: InvalidMapping, +) { + let hasWarnings = false + + if (unhandledFiles.length > 0) { + hasWarnings = true + console.log(`⚠️ Found ${unhandledFiles.length} unhandled legacy files:\n`) + + // Group by directory for better readability + const groupedFiles: Record = {} + unhandledFiles.forEach((file) => { + const dir = file.includes('/') ? file.split('/')[0] : 'root' + if (!groupedFiles[dir]) groupedFiles[dir] = [] + groupedFiles[dir].push(file) + }) + + // Display grouped warnings + Object.entries(groupedFiles) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([dir, files]) => { + console.log(`πŸ“ ${dir}/ (${files.length} files)`) + files.sort().forEach((file) => { + console.log(` β€’ ${file}`) + }) + console.log() + }) + } + + if (invalidMappings.length > 0) { + hasWarnings = true + if (unhandledFiles.length > 0) console.log('\n' + '─'.repeat(60) + '\n') + + console.log(`❌ Found ${invalidMappings.length} invalid mappings (destinations not in manifests):\n`) + + // Group by destination directory for better readability + const groupedMappings: Record = {} + invalidMappings.forEach((mapping) => { + const parts = mapping.destination.split('/') + const section = parts.length > 2 ? parts[2] : 'root' // /docs/[section]/... + if (!groupedMappings[section]) groupedMappings[section] = [] + groupedMappings[section].push(mapping) + }) + + // Display grouped invalid mappings + Object.entries(groupedMappings) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([section, mappings]) => { + console.log(`πŸ“‚ ${section}/ (${mappings.length} mappings)`) + mappings.forEach((mapping) => { + console.log(` β€’ ${mapping.source} β†’ ${mapping.destination} (${mapping.action})`) + }) + console.log() + }) + } + + if (pagesToCreate.length > 0) { + hasWarnings = true + if (unhandledFiles.length > 0 || invalidMappings.length > 0) console.log('\n' + '─'.repeat(60) + '\n') + + console.log(`πŸ“ Found ${pagesToCreate.length} pages that need new content:\n`) + + // Group by section for better readability + const groupedPages: Record = {} + pagesToCreate.forEach((path) => { + const parts = path.split('/') + const section = parts.length > 2 ? parts[2] : 'root' // /docs/[section]/... + if (!groupedPages[section]) groupedPages[section] = [] + groupedPages[section].push(path) + }) + + // Display grouped pages to create + Object.entries(groupedPages) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([section, paths]) => { + console.log(`πŸ“‚ ${section}/ (${paths.length} pages)`) + paths.sort().forEach((path) => { + console.log(` β€’ ${path}`) + }) + console.log() + }) + } + + if (!hasWarnings) { + console.log('βœ… All legacy files are handled, all mappings are valid, and all manifest pages have content sources') + return + } + + // Count files that need to be converted to examples + const convertToExampleFiles = Object.values(expandedMapping).filter( + (mapping) => mapping.action === 'convert-to-example', + ).length + + // Count files that will be generated + const generateFiles = Object.values(expandedMapping).filter((mapping) => mapping.action === 'generate').length + + console.log( + `\nπŸ“Š Summary: ${unhandledFiles.length} unhandled files, ${invalidMappings.length} invalid mappings, ${pagesToCreate.length} pages need new content, ${convertToExampleFiles} convert to examples, ${generateFiles} pages need to be generated, ${Object.keys(expandedMapping).length} files mapped`, + ) +} + +/** + * Check if a pattern is a glob pattern + * @param {string} pattern - The pattern to check + * @returns {boolean} - Whether the pattern contains glob characters + */ +function isGlobPattern(pattern: string) { + return pattern.includes('*') || pattern.includes('?') +} + +/** + * Convert a glob pattern to a regex + * @param {string} pattern - The glob pattern + * @returns {RegExp} - The corresponding regex + */ +function globToRegex(pattern: string) { + // Escape special regex characters except * and ? + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*/g, 'Β§DOUBLESTARΒ§') // Temporary placeholder + .replace(/\*/g, '[^/]*') // Single * matches anything except / + .replace(/Β§DOUBLESTARΒ§/g, '.*') // ** matches anything including / + .replace(/\?/g, '[^/]') // ? matches single char except / + + return new RegExp(`^${escaped}$`) +} + +/** + * Apply glob pattern to generate new path + * @param {string} filePath - The source file path + * @param {string} globPattern - The glob pattern used as key + * @param {string} newPathPattern - The new path pattern (may contain globs) + * @returns {string} - The generated new path + */ +function applyGlobMapping(filePath: string, globPattern: string, newPathPattern: string) { + // If newPathPattern doesn't contain globs, return as-is + if (!isGlobPattern(newPathPattern)) { + return newPathPattern + } + + // Handle ** glob replacement + if (globPattern.includes('**') && newPathPattern.includes('**')) { + const globPrefix = globPattern.split('**')[0] + const newPathPrefix = newPathPattern.split('**')[0] + const newPathSuffix = newPathPattern.split('**')[1] || '' + + if (filePath.startsWith(globPrefix)) { + const matchedPart = filePath.slice(globPrefix.length) + return newPathPrefix + matchedPart.replace(/\.mdx$/, '') + newPathSuffix + } + } + + // Handle single * replacement (simplified) + if (globPattern.includes('*') && newPathPattern.includes('*')) { + const globParts = globPattern.split('*') + const newPathParts = newPathPattern.split('*') + + if (globParts.length === 2 && newPathParts.length === 2) { + const [globPrefix, globSuffix] = globParts + const [newPrefix, newSuffix] = newPathParts + + if (filePath.startsWith(globPrefix) && filePath.endsWith(globSuffix)) { + const matchedPart = filePath.slice(globPrefix.length, -globSuffix.length || undefined) + return newPrefix + matchedPart + newSuffix + } + } + } + + return newPathPattern +} + +/** + * Expand glob patterns in mapping to actual file matches + * @param {Object} mapping - The mapping configuration + * @param {string[]} allFiles - All available files + * @returns {Object} - Expanded mapping with actual file paths + */ +function expandGlobMapping(mapping: Mapping, allFiles: string[]) { + const expandedMapping: Mapping = {} + + for (const [key, value] of Object.entries(mapping)) { + if (isGlobPattern(key)) { + const regex = globToRegex(key) + const matchingFiles = allFiles.filter((file) => regex.test(file)) + + for (const file of matchingFiles) { + expandedMapping[file] = { + ...value, + newPath: applyGlobMapping(file, key, value.newPath), + } + } + } else { + // Include non-glob patterns as-is + expandedMapping[key] = value + } + } + + return expandedMapping +} + +/** + * Find invalid mapping destinations (mapped to paths that don't exist in manifests) + * @param {Object} expandedMapping - Expanded mapping configuration + * @param {string[]} manifestPaths - All paths from manifest files + * @returns {string[]} Array of invalid mapping destinations + */ +function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) { + const manifestPathsSet = new Set(manifestPaths) + const invalidMappings: Array<{ source: string; destination: string; action: 'consolidate' | 'move' }> = [] + + for (const [sourcePath, mapping] of Object.entries(expandedMapping)) { + // Skip drop actions - they're intentionally removing paths + // Skip generate actions - they're creating new content that won't exist in manifests yet + // Skip convert-to-example actions - they're converting to examples that won't exist as specific manifest paths + if (mapping.action === 'drop' || mapping.action === 'generate' || mapping.action === 'convert-to-example') { + continue + } + + if (!manifestPathsSet.has(mapping.newPath)) { + invalidMappings.push({ + source: sourcePath, + destination: mapping.newPath, + action: mapping.action, + }) + } + } + + return invalidMappings +} + +type InvalidMapping = Awaited> + +/** + * Main function to parse docs content and check mappings + */ +async function main() { + if (!WARNINGS_ONLY) { + console.log('πŸ” Scanning for MDX files in ./docs') + } + + try { + const [mdxFiles, mapping, { paths: manifestPaths }] = await Promise.all([ + findMdxFiles(DOCS_PATH), + readMapping(), + readProposalManifestPaths(), + ]) + + // Expand glob patterns in mapping to actual file matches + const expandedMapping = expandGlobMapping(mapping, mdxFiles) + + const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) + const pagesToCreate = findPagesToCreate(manifestPaths, expandedMapping) + const invalidMappings = findInvalidMappings(expandedMapping, manifestPaths) + + if (WARNINGS_ONLY) { + console.log('πŸ” Checking for unhandled legacy files and pages to create...\n') + displayWarnings(unhandledFiles, pagesToCreate, expandedMapping, invalidMappings) + } else { + // Simplified output mode + console.log('πŸ“‹ UNHANDLED LEGACY FILES:\n') + if (unhandledFiles.length > 0) { + unhandledFiles.sort().forEach((file) => { + console.log(` β€’ ${file}`) + }) + } else { + console.log(' βœ… All legacy files are handled') + } + + console.log('\n❌ INVALID MAPPINGS (destinations not in manifests):\n') + if (invalidMappings.length > 0) { + invalidMappings.forEach((mapping) => { + console.log(` β€’ ${mapping.source} β†’ ${mapping.destination} (${mapping.action})`) + }) + } else { + console.log(' βœ… All mappings point to valid manifest paths') + } + + console.log('\nπŸ“ MANIFEST PATHS:\n') + manifestPaths.forEach((path) => { + console.log(` β€’ ${path}`) + }) + + // Count files that need to be converted to examples + const convertToExampleFiles = Object.values(expandedMapping).filter( + (mapping) => mapping.action === 'convert-to-example', + ).length + + // Count files that will be generated + const generateFiles = Object.values(expandedMapping).filter((mapping) => mapping.action === 'generate').length + + console.log('\nπŸ“Š SUMMARY:') + console.log(` β€’ Legacy files: ${mdxFiles.length}`) + console.log(` β€’ Manifest paths: ${manifestPaths.length}`) + console.log(` β€’ Original mapping entries: ${Object.keys(mapping).length}`) + console.log(` β€’ Expanded mapping entries: ${Object.keys(expandedMapping).length}`) + console.log(` β€’ Handled files: ${Object.keys(expandedMapping).length}`) + console.log(` β€’ Unhandled files: ${unhandledFiles.length}`) + console.log(` β€’ Pages needing new content: ${pagesToCreate.length}`) + console.log(` β€’ Convert to examples: ${convertToExampleFiles}`) + console.log(` β€’ Pages need to be generated: ${generateFiles}`) + console.log(` β€’ Invalid mappings: ${invalidMappings.length}`) + + if (unhandledFiles.length > 0 || pagesToCreate.length > 0) { + console.log('\nπŸ’‘ Run with DOCS_IA_WARNINGS_ONLY=true to see detailed warnings and grouped files') + } + } + } catch (error) { + console.error('❌ Error scanning docs:', error.message) + process.exit(1) + } +} + +// Run the script +main() From 28f40e2361183a0a02ae0de7dbfeb989fa8add66 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 17 Jul 2025 15:06:09 -0400 Subject: [PATCH 002/136] support top level and nested items --- package.json | 2 +- .../migration-assistant/generate-manifest.ts | 131 +++++++++++++----- 2 files changed, 101 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 16e13dee48..44cd79b627 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "vitest --silent", "typedoc:link": "node scripts/link-typedoc.mjs", "move-doc": "node scripts/move-doc.mjs", - "migration:generate-manifest": "tsx scripts/migration-assistant/generate-manifest.ts", + "migration:generate-manifest": "tsx watch scripts/migration-assistant/generate-manifest.ts", "migration:map-content": "tsx scripts/migration-assistant/map-content.ts" }, "devDependencies": { diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index 2131626df0..37892aecc2 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -20,25 +20,36 @@ const slugify = (str: string) => function parseMarkdownToManifest(content: string) { const lines = content.split('\n') const navigation: any[] = [] - let currentTopLevel: { title: string; items: any[] } | null = null + let currentTopLevelGroup: { title: string; items: any[] } | null = null let currentTopLevelCustomSlug: string | null = null + let currentSubGroup: { title: string; items: any[] } | null = null + let currentSubGroupCustomSlug: string | null = null let currentItemGroup: any[] = [] let pathStack: string[] = [] // Stack to track nested path components const finishCurrentItemGroup = () => { if (currentItemGroup && currentItemGroup.length > 0) { - if (currentTopLevel) { - currentTopLevel.items.push([...currentItemGroup]) + if (currentSubGroup) { + currentSubGroup.items.push([...currentItemGroup]) } currentItemGroup = [] } } - const finishCurrentTopLevel = () => { + const finishCurrentSubGroup = () => { finishCurrentItemGroup() - if (currentTopLevel) { - navigation.push([currentTopLevel]) - currentTopLevel = null + if (currentSubGroup && currentTopLevelGroup) { + currentTopLevelGroup.items.push([currentSubGroup]) + currentSubGroup = null + } + currentSubGroupCustomSlug = null + } + + const finishCurrentTopLevelGroup = () => { + finishCurrentSubGroup() + if (currentTopLevelGroup) { + navigation.push([currentTopLevelGroup]) + currentTopLevelGroup = null } currentTopLevelCustomSlug = null pathStack = [] @@ -55,14 +66,35 @@ function parseMarkdownToManifest(content: string) { continue } - // Skip top level sections (## Header) - we'll process their subsections + // Top level sections (## Header) - these become top-level groups if (trimmed.startsWith('## ')) { + finishCurrentTopLevelGroup() + const titleWithBrackets = trimmed.substring(3) + const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) + + let title: string, customSlug: string | null + if (bracketMatch) { + title = bracketMatch[1].trim() + customSlug = bracketMatch[2].trim() + } else { + title = titleWithBrackets.trim() + customSlug = null + } + + currentTopLevelGroup = { + title, + items: [], + } + currentTopLevelCustomSlug = customSlug + currentSubGroup = null + currentItemGroup = [] + pathStack = [] continue } - // Subsections (### Header) - these become top-level sections + // Subsections (### Header) - these become sub-groups within top-level groups if (trimmed.startsWith('### ')) { - finishCurrentTopLevel() + finishCurrentSubGroup() const titleWithBrackets = trimmed.substring(4) const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) @@ -75,11 +107,11 @@ function parseMarkdownToManifest(content: string) { customSlug = null } - currentTopLevel = { + currentSubGroup = { title, items: [], } - currentTopLevelCustomSlug = customSlug + currentSubGroupCustomSlug = customSlug currentItemGroup = [] pathStack = [] continue @@ -112,31 +144,66 @@ function parseMarkdownToManifest(content: string) { const itemSlug = customSlug || slugify(title) pathStack.push(itemSlug) - // Create the content item - const linkItem = { - title, - href: generateHref( - title, - currentTopLevel?.title, - undefined, - undefined, - itemSlug, - currentTopLevelCustomSlug || undefined, - ), - } + // If this is a level 0 item (no indentation), it could be either a simple item or a group header + if (indentLevel === 0) { + // Initialize item group if needed + if (!currentItemGroup) { + currentItemGroup = [] + } - // Initialize item group if needed - if (!currentItemGroup) { - currentItemGroup = [] + // Create the content item + const linkItem = { + title, + href: generateHref( + title, + currentSubGroup?.title, + undefined, + undefined, + itemSlug, + currentSubGroupCustomSlug || undefined, + ), + } + + // Add item to current group + currentItemGroup.push(linkItem) + } else { + // This is an indented item - we need to find or create a group for it + + // Get the parent item from the current item group + if (currentItemGroup && currentItemGroup.length > 0) { + const parentItem = currentItemGroup[currentItemGroup.length - 1] + + // Convert the parent item to a group if it's not already one + if (!parentItem.items) { + // Transform the parent from a simple item to a group + parentItem.items = [[]] + parentItem.collapse = true + delete parentItem.href // Groups don't have hrefs + } + + // Create the sub-item + const subItem = { + title, + href: generateHref( + title, + currentSubGroup?.title, + undefined, + undefined, + itemSlug, + currentSubGroupCustomSlug || undefined, + ), + } + + // Add the sub-item to the parent group's items + const lastSubGroup = parentItem.items[parentItem.items.length - 1] + lastSubGroup.push(subItem) + } } - - // Add item to current group - currentItemGroup.push(linkItem) } } // Finish any remaining content - finishCurrentTopLevel() + finishCurrentTopLevelGroup() return { navigation, @@ -190,6 +257,7 @@ export type ManifestItem = { export type ManifestGroup = { title: string items: Manifest + collapse?: boolean } export type Manifest = (ManifestItem | ManifestGroup)[][] @@ -205,6 +273,7 @@ const manifestGroup: z.ZodType = z .object({ title: z.string(), items: z.lazy(() => manifestSchema), + collapse: z.boolean().optional(), }) .strict() From b62ccef3ca18a339f9d4ed60b6c87a1af98ef5f8 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 17 Jul 2025 16:08:05 -0400 Subject: [PATCH 003/136] add watch script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 44cd79b627..7b0c46c389 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "typedoc:link": "node scripts/link-typedoc.mjs", "move-doc": "node scripts/move-doc.mjs", "migration:generate-manifest": "tsx watch scripts/migration-assistant/generate-manifest.ts", + "migration:generate-manifest:watch": "tsx watch scripts/migration-assistant/generate-manifest.ts --watch", "migration:map-content": "tsx scripts/migration-assistant/map-content.ts" }, "devDependencies": { From edeef172d1538daa8b666a5dc878b61dd362a486 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 09:57:06 -0400 Subject: [PATCH 004/136] Add mapping generation to build ci --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 79097c090e..6dd54f5ac1 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "npm run build", + "buildCommand": "npm run build && npm run migration:generate-manifest", "devCommand": "npm run dev", "installCommand": "npm install", "outputDirectory": "dist", From f288985420483a86c28b1251ded86215fe5ed39d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 09:57:46 -0400 Subject: [PATCH 005/136] run migration map checker in gh action --- .github/workflows/migration.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/migration.yml diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml new file mode 100644 index 0000000000..e0ce0abb2a --- /dev/null +++ b/.github/workflows/migration.yml @@ -0,0 +1,16 @@ +name: Run Migration Mapping Check + +on: + push: + paths: + - 'proposal-mapping.json' + - 'scripts/migration-assistant/map-content.ts' + - 'docs/**' + +jobs: + migration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm i + - run: npm run migration:map-content From 434907553c92fa0eb617fff6d8f89a3d56132c2d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 10:02:12 -0400 Subject: [PATCH 006/136] flip order --- scripts/migration-assistant/generate-manifest.ts | 2 +- vercel.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index 37892aecc2..ceb885580b 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -304,7 +304,7 @@ async function generateManifest() { // Read the ia-proposal.md file const proposalContent = await fs.readFile(PROPOSAL_PATH, 'utf-8') - console.log('βœ… Read ia-proposal.md') + console.log(`βœ… Read ${PROPOSAL_PATH}`) // Parse markdown to multiple navigation structures const manifest = parseMarkdownToManifest(proposalContent) diff --git a/vercel.json b/vercel.json index 6dd54f5ac1..85e13ea914 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "npm run build && npm run migration:generate-manifest", + "buildCommand": "npm run migration:generate-manifest && npm run build", "devCommand": "npm run dev", "installCommand": "npm install", "outputDirectory": "dist", From c3d5cc24d968b52d75158f495b9f7c3deb5095d0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 10:03:04 -0400 Subject: [PATCH 007/136] run on push to test ci --- .github/workflows/migration.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index e0ce0abb2a..e1457fd2c0 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -2,10 +2,13 @@ name: Run Migration Mapping Check on: push: - paths: - - 'proposal-mapping.json' - - 'scripts/migration-assistant/map-content.ts' - - 'docs/**' + +# on: +# push: +# paths: +# - 'proposal-mapping.json' +# - 'scripts/migration-assistant/map-content.ts' +# - 'docs/**' jobs: migration: From 7da6b581ef8fc1445daa63a6ff440403bd9ce14e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 10:04:07 -0400 Subject: [PATCH 008/136] generate the manifest first --- .github/workflows/migration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index e1457fd2c0..3353911547 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -16,4 +16,5 @@ jobs: steps: - uses: actions/checkout@v3 - run: npm i + - run: npm run migration:generate-manifest - run: npm run migration:map-content From d41ab9f11519876454ffeca1858eb17326d248b3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 10:06:52 -0400 Subject: [PATCH 009/136] remove watch from tsx cli --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b0c46c389..530779b630 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "vitest --silent", "typedoc:link": "node scripts/link-typedoc.mjs", "move-doc": "node scripts/move-doc.mjs", - "migration:generate-manifest": "tsx watch scripts/migration-assistant/generate-manifest.ts", + "migration:generate-manifest": "tsx scripts/migration-assistant/generate-manifest.ts", "migration:generate-manifest:watch": "tsx watch scripts/migration-assistant/generate-manifest.ts --watch", "migration:map-content": "tsx scripts/migration-assistant/map-content.ts" }, From be7e5582d765572c27de848f4e2e2cb38a38c4cc Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 10:46:58 -0400 Subject: [PATCH 010/136] define flags in this repo instead of clerk/clerk --- flags.json | 4 ++++ scripts/build-docs.test.ts | 11 +++++++++++ scripts/build-docs.ts | 22 ++++++++++++++++++++++ scripts/lib/config.ts | 13 +++++++++++++ scripts/lib/siteFlags.ts | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 flags.json create mode 100644 scripts/lib/siteFlags.ts diff --git a/flags.json b/flags.json new file mode 100644 index 0000000000..3703f53b7c --- /dev/null +++ b/flags.json @@ -0,0 +1,4 @@ +{ + "use-proposal-manifest": true, + "top-level-manifest-splitting": true +} diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 91434a8fc5..0b0e2e13ba 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -160,6 +160,7 @@ const baseConfig = { docs: {}, partials: {}, typedoc: {}, + tooltips: {}, }, manifestOptions: { wrapDefault: true, @@ -3729,6 +3730,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -3776,6 +3778,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -3827,6 +3830,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -3880,6 +3884,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -3932,6 +3937,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -3972,6 +3978,7 @@ title: Missing Description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -4027,6 +4034,7 @@ description: The page being linked to }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -4084,6 +4092,7 @@ description: This page has a description }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -4128,6 +4137,7 @@ description: Test page with partial }, docs: {}, typedoc: {}, + tooltips: {}, }, }), ) @@ -4313,6 +4323,7 @@ interface Client { }, partials: {}, typedoc: {}, + tooltips: {}, }, }), ) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 3f6be9656c..ee579de4e0 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -86,6 +86,7 @@ import { removeMdxSuffix } from './lib/utils/removeMdxSuffix' import { writeLLMs as generateLLMs, writeLLMsFull as generateLLMsFull, listOutputDocsFiles } from './lib/llms' import { VFile } from 'vfile' import { readTooltipsFolder, readTooltipsMarkdown, writeTooltips } from './lib/tooltips' +import { Flags, readSiteFlags, writeSiteFlags } from './lib/siteFlags' // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { @@ -123,6 +124,10 @@ async function main() { inputPath: '../docs/_tooltips', outputPath: '_tooltips', }, + siteFlags: { + inputPath: '../flags.json', + outputPath: '_flags.json', + }, ignoreLinks: ['/docs/quickstart'], ignorePaths: [ '/docs/core-1', @@ -256,6 +261,15 @@ export async function build(config: BuildConfig, store: Store = createBlankStore abortSignal?.throwIfAborted() + let siteFlags: Flags = {} + + if (config.siteFlags) { + siteFlags = await readSiteFlags(config) + console.info(`βœ“ Read ${Object.keys(siteFlags).length} site flags`) + } + + abortSignal?.throwIfAborted() + const apiErrorsFiles = await generateApiErrorDocs(config) if (!config.flags.skipApiErrors) { console.info('βœ“ Generated API Error MDX files') @@ -651,6 +665,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore await writeFile( 'manifest.json', JSON.stringify({ + flags: siteFlags, navigation: await traverseTree( { items: sdkScopedManifest }, async (item) => { @@ -972,6 +987,13 @@ template: wide abortSignal?.throwIfAborted() + if (config.siteFlags) { + await writeSiteFlags(config, siteFlags) + console.info(`βœ“ Wrote ${Object.keys(siteFlags).length} site flags to disk`) + } + + abortSignal?.throwIfAborted() + const flatSdkSpecificVFiles = sdkSpecificVFiles .flatMap(({ vFiles }) => vFiles) .filter((item): item is NonNullable => item !== null) diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index f16f077ad0..69836a632d 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -52,6 +52,10 @@ type BuildConfigOptions = { overviewPath?: string fullPath?: string } + siteFlags?: { + inputPath: string + outputPath: string + } flags?: { watch?: boolean controlled?: boolean @@ -154,6 +158,15 @@ export async function createConfig(config: BuildConfigOptions) { } : null, + siteFlags: config.siteFlags + ? { + inputPath: resolve(path.join(config.basePath, config.siteFlags.inputPath)), + inputPathRelative: config.siteFlags.inputPath, + outputPath: resolve(path.join(tempDist, config.siteFlags.outputPath)), + outputPathRelative: config.siteFlags.outputPath, + } + : null, + flags: { watch: config.flags?.watch ?? false, controlled: config.flags?.controlled ?? false, diff --git a/scripts/lib/siteFlags.ts b/scripts/lib/siteFlags.ts new file mode 100644 index 0000000000..5cb2052aed --- /dev/null +++ b/scripts/lib/siteFlags.ts @@ -0,0 +1,34 @@ +import type { BuildConfig } from './config' +import fs from 'node:fs/promises' +import path from 'node:path' +import { z } from 'zod' + +export async function readSiteFlags(config: BuildConfig) { + const { inputPath, outputPath } = config.siteFlags ?? {} + if (!inputPath || !outputPath) { + throw new Error('Site flags paths not configured') + } + + const flags = await fs.readFile(inputPath, 'utf-8') + + if (!flags) { + throw new Error(`Site flags not found at ${inputPath}`) + } + + return z.record(z.string(), z.boolean()).parse(JSON.parse(flags)) +} + +export type Flags = Awaited> + +export async function writeSiteFlags(config: BuildConfig, flags: Flags) { + const { outputPath } = config.siteFlags ?? {} + if (!outputPath) { + throw new Error('Prompts output path not configured') + } + + // Create the directory if it doesn't exist + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + + // Write the flags to the file + await fs.writeFile(outputPath, JSON.stringify(flags)) +} From e8fb05c4765b2ea9a4295cb1f01c8763e15885fa Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 11:21:10 -0400 Subject: [PATCH 011/136] Only enable ci on certain file changes --- .github/workflows/migration.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index 3353911547..6c9ce8808b 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -2,13 +2,11 @@ name: Run Migration Mapping Check on: push: - -# on: -# push: -# paths: -# - 'proposal-mapping.json' -# - 'scripts/migration-assistant/map-content.ts' -# - 'docs/**' + paths: + - 'proposal-mapping.json' + - 'proposal.md' + - 'scripts/migration-assistant/map-content.ts' + - 'docs/**' jobs: migration: From b22f559d15ba0b35c7d1fc244b326f093787655d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 11:26:41 -0400 Subject: [PATCH 012/136] disable feature flags --- flags.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flags.json b/flags.json index 3703f53b7c..3105743eed 100644 --- a/flags.json +++ b/flags.json @@ -1,4 +1,4 @@ { - "use-proposal-manifest": true, - "top-level-manifest-splitting": true + "use-proposal-manifest": false, + "top-level-manifest-splitting": false } From c32a42426ca098477a2ae2d1e22b3bca827d42ff Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 11:29:22 -0400 Subject: [PATCH 013/136] fix up tests --- scripts/build-docs.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 0b0e2e13ba..e00fed0f29 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -226,6 +226,7 @@ Testing with a simple page.`) expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + flags: {}, navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }) }) @@ -564,6 +565,7 @@ title: Simple Test const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) expect(manifest).toEqual({ + flags: {}, navigation: [ [ { @@ -668,6 +670,7 @@ title: Item 2 const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) expect(manifest).toEqual({ + flags: {}, navigation: [ [ { @@ -727,6 +730,7 @@ title: Item 1 const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) expect(manifest).toEqual({ + flags: {}, navigation: [ [ { @@ -823,6 +827,7 @@ title: Item 1 const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) expect(manifest).toEqual({ + flags: {}, navigation: [ [ { @@ -962,6 +967,7 @@ This is a normal document.`, // Check that the manifest contains the target="_blank" attribute const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) expect(manifest).toEqual({ + flags: {}, navigation: [ [ { title: 'Normal Link', href: '/docs/normal-link' }, @@ -1023,6 +1029,7 @@ title: Quickstart expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + flags: {}, navigation: [ [ { @@ -1083,6 +1090,7 @@ Testing with a simple page.`, ) expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + flags: {}, navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]], }) @@ -1150,6 +1158,7 @@ Testing with a simple page.`, ) expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + flags: {}, navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], }) @@ -1441,6 +1450,7 @@ Content for React users.`, ) expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + flags: {}, navigation: [ [ { From 7c8f48b90387ec9a261e08a9ce54cc45b574c37e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 23:34:53 +0800 Subject: [PATCH 014/136] Potential fix for code scanning alert no. 10: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/migration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index 6c9ce8808b..418b87329e 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -1,5 +1,8 @@ name: Run Migration Mapping Check +permissions: + contents: read + on: push: paths: From b919b0f33d6902447fde140295876a47aaa48ce5 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 12:40:20 -0400 Subject: [PATCH 015/136] add support for 3rd level dropdown --- .../migration-assistant/generate-manifest.ts | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index ceb885580b..4e07868ac7 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -144,60 +144,64 @@ function parseMarkdownToManifest(content: string) { const itemSlug = customSlug || slugify(title) pathStack.push(itemSlug) - // If this is a level 0 item (no indentation), it could be either a simple item or a group header - if (indentLevel === 0) { - // Initialize item group if needed - if (!currentItemGroup) { - currentItemGroup = [] + // Helper function to find or create a parent container at a specific nesting level + const findOrCreateParentContainer = (targetLevel: number): any[] => { + if (targetLevel === 0) { + // Level 0: return the current item group + if (!currentItemGroup) { + currentItemGroup = [] + } + return currentItemGroup } - // Create the content item - const linkItem = { - title, - href: generateHref( - title, - currentSubGroup?.title, - undefined, - undefined, - itemSlug, - currentSubGroupCustomSlug || undefined, - ), + // For nested levels, navigate through the structure + let currentContainer = currentItemGroup + if (!currentContainer || currentContainer.length === 0) { + return [] } - // Add item to current group - currentItemGroup.push(linkItem) - } else { - // This is an indented item - we need to find or create a group for it + // Navigate through each level of nesting + for (let level = 1; level <= targetLevel; level++) { + const parentItem = currentContainer[currentContainer.length - 1] - // Get the parent item from the current item group - if (currentItemGroup && currentItemGroup.length > 0) { - const parentItem = currentItemGroup[currentItemGroup.length - 1] + if (!parentItem) { + return [] + } // Convert the parent item to a group if it's not already one if (!parentItem.items) { - // Transform the parent from a simple item to a group parentItem.items = [[]] parentItem.collapse = true delete parentItem.href // Groups don't have hrefs } - // Create the sub-item - const subItem = { - title, - href: generateHref( - title, - currentSubGroup?.title, - undefined, - undefined, - itemSlug, - currentSubGroupCustomSlug || undefined, - ), - } - - // Add the sub-item to the parent group's items + // Get the last sub-group within this parent const lastSubGroup = parentItem.items[parentItem.items.length - 1] - lastSubGroup.push(subItem) + currentContainer = lastSubGroup } + + return currentContainer + } + + // Create the new item + const newItem = { + title, + href: generateHref( + title, + currentSubGroup?.title, + undefined, + undefined, + itemSlug, + currentSubGroupCustomSlug || undefined, + ), + } + + // Find the appropriate container for this indentation level + const targetContainer = findOrCreateParentContainer(indentLevel) + + // Add the item to the target container + if (targetContainer) { + targetContainer.push(newItem) } } } From 794d2185db6092c961bdad7e60dbdb1c75e4cf63 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 13:16:23 -0400 Subject: [PATCH 016/136] Add a readme for new scripts --- scripts/migration-assistant/README.md | 220 +++++++++++++++++++++ scripts/migration-assistant/map-content.ts | 5 +- 2 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 scripts/migration-assistant/README.md diff --git a/scripts/migration-assistant/README.md b/scripts/migration-assistant/README.md new file mode 100644 index 0000000000..415526112d --- /dev/null +++ b/scripts/migration-assistant/README.md @@ -0,0 +1,220 @@ +# Migration Assistant + +The Migration Assistant is a set of tools designed to help manage the documentation migration for Clerk Docs. It helps convert markdown-based proposals into structured manifests and validates content mappings between the legacy and new documentation structures. + +## Overview + +The migration assistant consists of two main scripts that work together to: + +1. **Generate navigation manifests** from markdown proposals +2. **Validate content mappings** between legacy and new documentation structures +3. **Identify gaps** in content migration planning + +## Scripts + +### 1. `generate-manifest.ts` + +Converts a markdown-based proposal into a structured JSON manifest. + +**What it does:** + +- Reads `proposal.md` (markdown file with the new IA structure) +- Parses markdown headers (`##`, `###`) and list items (`-`) into a hierarchical navigation structure +- Supports custom URL slugs using bracket notation: `Title [custom-slug]` +- Generates `public/manifest.proposal.json` with the navigation structure +- Validates the generated manifest against a Zod schema + +**Input:** `proposal.md` +**Output:** `public/manifest.proposal.json` + +### 2. `map-content.ts` + +Analyzes the relationship between legacy content and the proposed new structure. + +**What it does:** + +- Reads `proposal-mapping.json` (mapping of legacy files to new locations) +- Scans all existing `.mdx` files in the `docs/` directory +- Compares mappings against the proposed manifest structure +- Identifies issues and provides detailed reports + +**Reports on:** + +- **Unhandled legacy files** - Files that exist but aren't mapped anywhere +- **Invalid mappings** - Mappings pointing to destinations that don't exist in the manifest +- **Pages needing new content** - Manifest paths that don't have content sources +- **Summary statistics** - Counts of different mapping actions + +**Input:** `proposal-mapping.json`, `public/manifest.proposal.json`, existing `.mdx` files +**Output:** Console reports and warnings + +## Key Files + +### Configuration Files + +- **`proposal.md`** - Markdown file defining the new structure +- **`proposal-mapping.json`** - JSON file mapping legacy content to new locations +- **`flags.json`** - Feature flags (contains `use-proposal-manifest` flag) + +### Generated Files + +- **`public/manifest.proposal.json`** - Generated navigation manifest from the proposal + +## Mapping Actions + +The `proposal-mapping.json` file supports several action types: + +- **`move`** - Move content to a new location as-is +- **`consolidate`** - Combine multiple files into a single new location +- **`generate`** - Create new content (placeholder for content that needs to be written) +- **`convert-to-example`** - Convert documentation into github example +- **`drop`** - Remove content entirely + +### Mapping Format + +```json +{ + "path/to/legacy/file.mdx": { + "newPath": "/docs/new/location", + "action": "move" + }, + "glob/pattern/**": { + "newPath": "/docs/new/section/**", + "action": "generate" + } +} +``` + +**Glob Pattern Support:** + +- `**` - Matches any number of directories and files +- `*` - Matches any characters except `/` within a single path segment +- Patterns are expanded to match actual files automatically + +## Usage + +### Running the Scripts + +```bash +# Generate manifest from proposal.md +npm run migration:generate-manifest + +# Generate manifest with file watching (auto-regenerate on changes) +npm run migration:generate-manifest:watch + +# Analyze content mappings +npm run migration:map-content +``` + +### Environment Variables + +- **`DOCS_WARNINGS_ONLY=true`** - Show detailed grouped warnings instead of simplified output + +```bash +# Detailed warnings mode +DOCS_WARNINGS_ONLY=true npm run migration:map-content +``` + +## Workflow + +### 1. Define New Information Architecture + +Edit `proposal.md` to define your new documentation structure: + +```markdown +## Section Name [custom-slug] + +### Subsection + +- Page Title [custom-page-slug] +- Another Page + - Nested Page +``` + +### 2. Generate Manifest + +```bash +npm run migration:generate-manifest +``` + +This creates `public/manifest.proposal.json` with the navigation structure. + +### 3. Map Legacy Content + +Edit `proposal-mapping.json` to map existing content to new locations: + +```json +{ + "old/path/file.mdx": { + "newPath": "/docs/new/location", + "action": "move" + } +} +``` + +### 4. Validate Mappings + +```bash +npm run migration:map-content +``` + +Review the output for: + +- Unhandled files that need mapping +- Invalid mappings pointing to non-existent destinations +- Missing content for manifest pages + +### 5. Iterate + +Repeat steps 1-4 until all content is properly mapped and validated. + +## Output Examples + +### Standard Output + +``` +πŸ“‹ UNHANDLED LEGACY FILES: + β€’ path/to/unmapped/file.mdx + +❌ INVALID MAPPINGS: + β€’ source.mdx β†’ /docs/nonexistent/path (move) + +πŸ“ MANIFEST PATHS: + β€’ /docs/section/page + +πŸ“Š SUMMARY: + β€’ Legacy files: 150 + β€’ Manifest paths: 120 + β€’ Handled files: 140 + β€’ Unhandled files: 10 +``` + +### Detailed Warnings Mode + +With `DOCS_WARNINGS_ONLY=true`: + +``` +⚠️ Found 5 unhandled legacy files: + +πŸ“ authentication/ (3 files) + β€’ authentication/legacy-page.mdx + β€’ authentication/old-guide.mdx + +πŸ“ guides/ (2 files) + β€’ guides/deprecated.mdx +``` + +## Tips + +1. **Start broad** - Use glob patterns in mappings to handle multiple files at once +2. **Use the watch mode** while iterating on the proposal structure +3. **Run validation frequently** to catch mapping issues early +4. **Group related consolidations** to make the migration easier to manage +5. **Document custom slugs** clearly in your proposal for better URL structure + +## Troubleshooting + +- **Validation errors**: Check that your `proposal.md` follows the expected markdown structure +- **Glob patterns not working**: Ensure patterns use `**` for recursive matching and avoid shell glob syntax +- **Missing files in output**: Check that files aren't excluded by the `**/_*/**` ignore pattern +- **Invalid manifest structure**: Run the generate script to see detailed Zod validation errors diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index cb4c185971..d890e441a5 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -1,3 +1,4 @@ +import 'dotenv/config' import { readFile } from 'fs/promises' import { glob } from 'glob' import { join, relative } from 'path' @@ -10,7 +11,7 @@ const MANIFEST_PATH = join(process.cwd(), './public/manifest.json') const DOCS_PATH = join(process.cwd(), './docs') // Environment variable to control output -const WARNINGS_ONLY = process.env.DOCS_IA_WARNINGS_ONLY === 'true' +const WARNINGS_ONLY = process.env.DOCS_WARNINGS_ONLY === 'true' /** * Read and parse the mapping.json file @@ -425,7 +426,7 @@ async function main() { console.log(` β€’ Invalid mappings: ${invalidMappings.length}`) if (unhandledFiles.length > 0 || pagesToCreate.length > 0) { - console.log('\nπŸ’‘ Run with DOCS_IA_WARNINGS_ONLY=true to see detailed warnings and grouped files') + console.log('\nπŸ’‘ Run with DOCS_WARNINGS_ONLY=true to see detailed warnings and grouped files') } } } catch (error) { From 8797e04b75578682af75cd87332722d351b4beda Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 18 Jul 2025 13:18:11 -0400 Subject: [PATCH 017/136] Add dotenv --- package-lock.json | 14 ++++++++++++++ package.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index ca6250fbed..7748d9db5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^22.13.2", "chokidar": "^4.0.3", "concurrently": "^8.2.2", + "dotenv": "^17.2.0", "glob": "^11.0.1", "jsonc-parser": "^3.3.1", "prettier": "^3.2.5", @@ -2014,6 +2015,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index 530779b630..3692928ef7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/node": "^22.13.2", "chokidar": "^4.0.3", "concurrently": "^8.2.2", + "dotenv": "^17.2.0", "glob": "^11.0.1", "jsonc-parser": "^3.3.1", "prettier": "^3.2.5", From 6d75db855f87b19e03e99afece7eeb3e5b9346d1 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Fri, 18 Jul 2025 21:05:25 -0400 Subject: [PATCH 018/136] activate top-level manifest splitting and proposal generation --- flags.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flags.json b/flags.json index 3105743eed..3703f53b7c 100644 --- a/flags.json +++ b/flags.json @@ -1,4 +1,4 @@ { - "use-proposal-manifest": false, - "top-level-manifest-splitting": false + "use-proposal-manifest": true, + "top-level-manifest-splitting": true } From 2db149f213f902900470a6cb738a6264e4d9b15b Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Fri, 18 Jul 2025 21:05:42 -0400 Subject: [PATCH 019/136] add support for icons in proposal.md. add more to proposal --- proposal.md | 31 ++- .../migration-assistant/generate-manifest.ts | 201 ++++++++++++++---- 2 files changed, 190 insertions(+), 42 deletions(-) diff --git a/proposal.md b/proposal.md index 2db5ce23cf..75ca065c06 100644 --- a/proposal.md +++ b/proposal.md @@ -1,13 +1,34 @@ # Top Level Links -- Home -- Getting Started -- Guides -- Examples -- Reference +- Home (home) +- Getting Started (globe) +- Guides (globe) +- Examples (globe) +- Reference (globe) ## Home +### SDK Reference + +- Next.js (nextjs) +- React (react) +- Backend SDK (clerk) +- Node.js (nodejs) +- Go (go) +- Tanstack Start (tanstack) +- Ruby / Rails (ruby) +- Community SDKs + - Vue + - Svelte + - Hono + - Astro + +### API Reference + +- Overview +- Backend API +- Frontend API + ### Core Concepts - How Clerk Works diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index 4e07868ac7..d992b60cbb 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -5,6 +5,103 @@ import { z } from 'zod' const PROPOSAL_PATH = path.join(process.cwd(), './proposal.md') const OUTPUT_PATH = path.join(process.cwd(), './public/manifest.proposal.json') +// Valid icon names from the schema +const VALID_ICONS = [ + 'apple', + 'application-2', + 'arrow-up-circle', + 'astro', + 'angular', + 'block', + 'bolt', + 'book', + 'box', + 'c-sharp', + 'chart', + 'checkmark-circle', + 'chrome', + 'clerk', + 'code-bracket', + 'cog-6-teeth', + 'door', + 'elysia', + 'expressjs', + 'globe', + 'go', + 'home', + 'hono', + 'javascript', + 'koa', + 'link', + 'linkedin', + 'lock', + 'nextjs', + 'nodejs', + 'plug', + 'plus-circle', + 'python', + 'react', + 'redwood', + 'remix', + 'react-router', + 'rocket', + 'route', + 'ruby', + 'rust', + 'speedometer', + 'stacked-rectangle', + 'solid', + 'svelte', + 'tanstack', + 'user-circle', + 'user-dotted-circle', + 'vue', + 'x', + 'expo', + 'nuxt', + 'fastify', +] as const + +type ValidIcon = (typeof VALID_ICONS)[number] + +/** + * Parse title, icon, and custom slug from text + * Supports formats like: + * - "Title (icon) [slug]" + * - "Title (icon)" + * - "Title [slug]" + * - "Title" + */ +function parseTextWithIconAndSlug(text: string): { + title: string + icon: ValidIcon | null + customSlug: string | null +} { + // Match pattern: Title (icon) [slug] or any combination + const match = text.match(/^(.+?)(?:\s*\(([^)]+)\))?(?:\s*\[([^\]]+)\])?$/) + + if (!match) { + return { title: text.trim(), icon: null, customSlug: null } + } + + const [, titlePart, iconPart, slugPart] = match + const title = titlePart.trim() + const icon = iconPart?.trim() + const customSlug = slugPart?.trim() + + // Validate icon if provided + const validIcon = icon && VALID_ICONS.includes(icon as ValidIcon) ? (icon as ValidIcon) : null + if (icon && !validIcon) { + console.warn(`Warning: Invalid icon "${icon}" for "${title}". Valid icons: ${VALID_ICONS.join(', ')}`) + } + + return { + title, + icon: validIcon, + customSlug: customSlug || null, + } +} + // Convert to lowercase and replace spaces with hyphens const slugify = (str: string) => str @@ -20,12 +117,14 @@ const slugify = (str: string) => function parseMarkdownToManifest(content: string) { const lines = content.split('\n') const navigation: any[] = [] + const topLevelLinksMap = new Map() // Store top-level links info let currentTopLevelGroup: { title: string; items: any[] } | null = null let currentTopLevelCustomSlug: string | null = null let currentSubGroup: { title: string; items: any[] } | null = null let currentSubGroupCustomSlug: string | null = null let currentItemGroup: any[] = [] let pathStack: string[] = [] // Stack to track nested path components + let inTopLevelLinksSection = false // Track if we're in the Top Level Links section const finishCurrentItemGroup = () => { if (currentItemGroup && currentItemGroup.length > 0) { @@ -61,25 +160,35 @@ function parseMarkdownToManifest(content: string) { // Skip empty lines if (!trimmed) continue - // Skip the "Top Level Links" section and its content + // Handle the "Top Level Links" section if (trimmed === '# Top Level Links') { + inTopLevelLinksSection = true + continue + } + + // Exit Top Level Links section when we hit a new section + if (inTopLevelLinksSection && (trimmed.startsWith('## ') || trimmed.startsWith('# '))) { + inTopLevelLinksSection = false + } + + // Process Top Level Links items + if (inTopLevelLinksSection && trimmed.startsWith('- ')) { + const titleWithIconAndSlug = trimmed.substring(2) + const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) + + // Store top-level link info for later merging with sections + topLevelLinksMap.set(title, { + icon: icon || undefined, + customSlug: customSlug || undefined, + }) continue } // Top level sections (## Header) - these become top-level groups if (trimmed.startsWith('## ')) { finishCurrentTopLevelGroup() - const titleWithBrackets = trimmed.substring(3) - const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) - - let title: string, customSlug: string | null - if (bracketMatch) { - title = bracketMatch[1].trim() - customSlug = bracketMatch[2].trim() - } else { - title = titleWithBrackets.trim() - customSlug = null - } + const titleWithIconAndSlug = trimmed.substring(3) + const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) currentTopLevelGroup = { title, @@ -95,17 +204,8 @@ function parseMarkdownToManifest(content: string) { // Subsections (### Header) - these become sub-groups within top-level groups if (trimmed.startsWith('### ')) { finishCurrentSubGroup() - const titleWithBrackets = trimmed.substring(4) - const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) - - let title: string, customSlug: string | null - if (bracketMatch) { - title = bracketMatch[1].trim() - customSlug = bracketMatch[2].trim() - } else { - title = titleWithBrackets.trim() - customSlug = null - } + const titleWithIconAndSlug = trimmed.substring(4) + const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) currentSubGroup = { title, @@ -123,18 +223,9 @@ function parseMarkdownToManifest(content: string) { const leadingSpaces = line.length - line.trimStart().length const indentLevel = Math.floor(leadingSpaces / 2) // Assuming 2 spaces per indent level - const titleWithBrackets = trimmed.substring(2) - // Parse bracket syntax for list items - const bracketMatch = titleWithBrackets.match(/^(.+?)\s*\[([^\]]+)\]$/) - - let title: string, customSlug: string | null - if (bracketMatch) { - title = bracketMatch[1].trim() - customSlug = bracketMatch[2].trim() - } else { - title = titleWithBrackets.trim() - customSlug = null - } + const titleWithIconAndSlug = trimmed.substring(2) + // Parse icon and slug syntax for list items + const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) // Update pathStack based on indentation level // Keep only the path components for levels above the current one @@ -184,7 +275,7 @@ function parseMarkdownToManifest(content: string) { } // Create the new item - const newItem = { + const newItem: any = { title, href: generateHref( title, @@ -196,6 +287,11 @@ function parseMarkdownToManifest(content: string) { ), } + // Add icon if provided + if (icon) { + newItem.icon = icon + } + // Find the appropriate container for this indentation level const targetContainer = findOrCreateParentContainer(indentLevel) @@ -209,11 +305,38 @@ function parseMarkdownToManifest(content: string) { // Finish any remaining content finishCurrentTopLevelGroup() + // Merge top-level link info (icons, custom slugs) with section groups + const finalNavigation = navigation.map((navGroup) => { + const group = navGroup[0] + if (group && group.title) { + const topLevelInfo = topLevelLinksMap.get(group.title) + if (topLevelInfo) { + // Add icon from top-level links if available + if (topLevelInfo.icon) { + group.icon = topLevelInfo.icon + } + // Note: custom slug from top-level links could override section slug if needed + } + } + return navGroup + }) + return { - navigation, + navigation: finalNavigation, } } +/** + * Generate href for top-level navigation items + * @param {string} title - The item title + * @param {string} customSlug - The custom slug for the item + * @returns {string} - The generated href + */ +function generateTopLevelHref(title: string, customSlug?: string) { + const itemSlug = customSlug || slugify(title) + return `/docs/${itemSlug}` +} + /** * Generate href from title and context * @param {string} title - The item title @@ -256,12 +379,14 @@ function generateHref( export type ManifestItem = { title: string href: string + icon?: ValidIcon } export type ManifestGroup = { title: string items: Manifest collapse?: boolean + icon?: ValidIcon } export type Manifest = (ManifestItem | ManifestGroup)[][] @@ -270,6 +395,7 @@ const manifestItem: z.ZodType = z .object({ title: z.string(), href: z.string(), + icon: z.enum(VALID_ICONS).optional(), }) .strict() @@ -278,6 +404,7 @@ const manifestGroup: z.ZodType = z title: z.string(), items: z.lazy(() => manifestSchema), collapse: z.boolean().optional(), + icon: z.enum(VALID_ICONS).optional(), }) .strict() From 15b54b653d2c2d7ea4876293d9063cbbb6d010de Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Fri, 18 Jul 2025 21:31:44 -0400 Subject: [PATCH 020/136] revise proposal --- proposal.md | 88 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/proposal.md b/proposal.md index 75ca065c06..c2f3f5f05f 100644 --- a/proposal.md +++ b/proposal.md @@ -1,9 +1,9 @@ # Top Level Links - Home (home) -- Getting Started (globe) -- Guides (globe) -- Examples (globe) +- Getting Started (checkmark-circle) +- Guides (book) +- Examples (plus-circle) - Reference (globe) ## Home @@ -31,6 +31,7 @@ ### Core Concepts +- Overview - How Clerk Works - Integrate Clerk - Clerk Objects @@ -60,17 +61,16 @@ ## Guides [guides] -### Securing your App [secure] +### Configuring your App [configure] -- Restricting Access -- Multifactor Authentication (MFA) [mfa] -- Bot Detection -- Banning Users -- Prevent brute force attacks -- Re-verification (Step-up) -- Legal Compliance -- Security Best Practices [best-practices] -- Session Options +- Authentication Strategies [auth-strategies] + - Social Connections + - Enterprise Connections + - Web3 +- Session Token customization +- Syncing data with webhooks +- Backend Requests +- Integrations ### Managing Users [users] @@ -80,6 +80,15 @@ - Syncing Clerk Data [syncing] - Impersonation +### Customizing Clerk + +- UI Customization (Appearance Prop) [appearance-prop] +- Account Portal +- Adding items to UI Components [adding-items] +- Email and SMS Templates +- Localization (i18n) [localization] +- Clerk Elements (beta) [elements] + ### B2B - Overview @@ -94,25 +103,17 @@ - Billing for B2C [for-b2c] - Billing for B2B [for-b2b] -### Customizing Clerk - -- UI Customization (Appearance Prop) [appearance-prop] -- Account Portal -- Adding items to UI Components [adding-items] -- Email and SMS Templates -- Localization (i18n) [localization] -- Clerk Elements (beta) [elements] - -### Configuring your App [configure] +### Securing your App [secure] -- Authentication Strategies [auth-strategies] - - Social Connections - - Enterprise Connections - - Web3 -- Session Token customization -- Syncing data with webhooks -- Backend Requests -- Integrations +- Restricting Access +- Multifactor Authentication (MFA) [mfa] +- Bot Detection +- Banning Users +- Prevent brute force attacks +- Re-verification (Step-up) +- Legal Compliance +- Security Best Practices [best-practices] +- Session Options ### Clerk Dashboard @@ -148,8 +149,27 @@ ## Reference [reference] -### API Reference [reference] +### General [reference] -- SDK Reference [sdk] - UI Components [components] -- API Reference + +### SDK Reference + +- Next.js (nextjs) +- React (react) +- Backend SDK (clerk) +- Node.js (nodejs) +- Go (go) +- Tanstack Start (tanstack) +- Ruby / Rails (ruby) +- Community SDKs + - Vue + - Svelte + - Hono + - Astro + +### HTTP API Reference + +- Frontend API [fapi] +- Backend API [bapi] +- Management API [mapi] From e832559ba8b8b4c9ad55bf846c3ec16d7a41bc68 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Mon, 21 Jul 2025 09:50:51 -0400 Subject: [PATCH 021/136] proposal checkpoint --- proposal.md | 84 ++++++++++++++++++----------------------------------- 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/proposal.md b/proposal.md index c2f3f5f05f..91c6fedcb2 100644 --- a/proposal.md +++ b/proposal.md @@ -1,65 +1,16 @@ # Top Level Links -- Home (home) +- Home (book) - Getting Started (checkmark-circle) -- Guides (book) - Examples (plus-circle) - Reference (globe) -## Home +## Home [guides] -### SDK Reference - -- Next.js (nextjs) -- React (react) -- Backend SDK (clerk) -- Node.js (nodejs) -- Go (go) -- Tanstack Start (tanstack) -- Ruby / Rails (ruby) -- Community SDKs - - Vue - - Svelte - - Hono - - Astro - -### API Reference - -- Overview -- Backend API -- Frontend API - -### Core Concepts - -- Overview -- How Clerk Works -- Integrate Clerk -- Clerk Objects -- Security at Clerk [security] -- System Limits -- AI Prompt Library [ai-prompts] -- Multi-tenant Architecture - -## Getting Started [getting-started] - -### Quickstart - -- App Router -- Pages Router - -### Clerk Dashboard - -- Setting up your Clerk account -- Configure your application - -### Next Steps +### Getting Started -- Custom sign-in/up pages -- Protect specific routes -- Read user and session data -- Add middleware - -## Guides [guides] +- App Router Quickstart +- Pages Router Quickstart ### Configuring your App [configure] @@ -141,6 +92,29 @@ - Progressive Sign Ups - Troubleshooting +## Getting Started [getting-started] + +### Quickstart + +- App Router +- Pages Router + +### Clerk Dashboard + +- Setting up your Clerk account +- Configure your application + +### Next Steps + +- Custom sign-in/up pages +- Protect specific routes +- Read user and session data +- Add middleware + +### More + +- View all guides + ## Examples [examples] ### Testing @@ -151,7 +125,7 @@ ### General [reference] -- UI Components [components] +- UI Components (box) [components] ### SDK Reference From 580f0d09a07725007ee08fffdd036f6e09151888 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Mon, 21 Jul 2025 13:15:21 -0400 Subject: [PATCH 022/136] proposal checkpoint --- proposal.md | 115 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/proposal.md b/proposal.md index 91c6fedcb2..3a0365b520 100644 --- a/proposal.md +++ b/proposal.md @@ -1,9 +1,12 @@ # Top Level Links -- Home (book) +- Home (home) - Getting Started (checkmark-circle) -- Examples (plus-circle) -- Reference (globe) +- Examples (code-bracket) +- API Reference (globe) + - SDK Reference + - HTTP API Reference + - UI Components ## Home [guides] @@ -26,21 +29,20 @@ ### Managing Users [users] - Managing Users [managing] -- Reading Clerk Data [reading] - Extending Clerk Data [extending] - Syncing Clerk Data [syncing] -- Impersonation +- Invitations +- User Impersonation ### Customizing Clerk -- UI Customization (Appearance Prop) [appearance-prop] -- Account Portal +- UI Customization [appearance-prop] - Adding items to UI Components [adding-items] - Email and SMS Templates -- Localization (i18n) [localization] -- Clerk Elements (beta) [elements] +- Localization - i18n [localization] +- Clerk Elements - `BETA` [elements] -### B2B +### B2B / Organizations [b2b] - Overview - Managing Organizations [managing-orgs] @@ -56,15 +58,24 @@ ### Securing your App [secure] +- Overview - Restricting Access -- Multifactor Authentication (MFA) [mfa] +- Multifactor Authentication - MFA [mfa] - Bot Detection - Banning Users - Prevent brute force attacks -- Re-verification (Step-up) +- Re-verification / Step-up - Legal Compliance -- Security Best Practices [best-practices] - Session Options +- Security Best Practices [best-practices] + - Overview + - Security at Clerk + - Vulnerability disclosure policy + - Clerk telemetry + - CSRF protection + - CSP headers + - Fixation protection + - Password protection and rules ### Clerk Dashboard @@ -92,6 +103,17 @@ - Progressive Sign Ups - Troubleshooting +### Core Concepts + +- Overview +- How Clerk Works +- Integrate Clerk +- Clerk Objects +- Security at Clerk [security] +- System Limits +- AI Prompt Library [ai-prompts] +- Multi-tenant Architecture + ## Getting Started [getting-started] ### Quickstart @@ -99,11 +121,6 @@ - App Router - Pages Router -### Clerk Dashboard - -- Setting up your Clerk account -- Configure your application - ### Next Steps - Custom sign-in/up pages @@ -111,39 +128,53 @@ - Read user and session data - Add middleware -### More +### Clerk Dashboard -- View all guides +- Setting up your Clerk account +- Configure your application + +### Learn More + +- View all guides (book) ## Examples [examples] -### Testing +### Integration Type + +- UI Components (box) +- Hooks (plug) + +### Sign In -- Test Link +- Sign in wth Email & Password +- Sign In with OAuth +- Sign in with Email Code +- Sign in with Email Link +- Sign in with Passkeys +- Multi-Factor Authentication -## Reference [reference] +## API Reference -### General [reference] +### App Router -- UI Components (box) [components] +- `auth()` +- `currentUser()` +- Route Handlers +- Server Actions -### SDK Reference +### Pages Router -- Next.js (nextjs) -- React (react) -- Backend SDK (clerk) -- Node.js (nodejs) -- Go (go) -- Tanstack Start (tanstack) -- Ruby / Rails (ruby) -- Community SDKs - - Vue - - Svelte - - Hono - - Astro +- `getAuth()` +- `buildClerkProps()` -### HTTP API Reference +### Hooks -- Frontend API [fapi] -- Backend API [bapi] -- Management API [mapi] +- `useUser()` +- `useClerk()` +- `useAuth()` +- `useSignIn()` +- `useSignUp()` +- `useSession()` +- `useSessionList()` +- `useOrganization()` +- `useOrganizationList()` From d1c0c097aa4a90ff9cd69f2a0942771d78e6a6ab Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Mon, 21 Jul 2025 13:55:42 -0400 Subject: [PATCH 023/136] add sub top-level categories under reference --- proposal.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/proposal.md b/proposal.md index 3a0365b520..0a9598ba38 100644 --- a/proposal.md +++ b/proposal.md @@ -178,3 +178,92 @@ - `useSessionList()` - `useOrganization()` - `useOrganizationList()` + +## SDK Reference + +### App Router + +- `auth()` +- `currentUser()` +- Route Handlers +- Server Actions + +### Pages Router + +- `getAuth()` +- `buildClerkProps()` + +### Hooks + +- `useUser()` +- `useClerk()` +- `useAuth()` +- `useSignIn()` +- `useSignUp()` +- `useSession()` +- `useSessionList()` +- `useOrganization()` +- `useOrganizationList()` + +## HTTP API Reference + +### API Reference + +- Overview +- Backend API +- Frontend API +- Management API + +## UI Components + +### General + +- Overview +- `` + +### Authentication Components + +- `` +- `` +- `` + +### User Components + +- `` +- `` + +### Organization Components + +- `` +- `` +- `` +- `` + +### Waitlist Component + +- `` + +### Billing Components + +- `` + +### Control Components + +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` + +### Unstyled Components + +- `` +- `` +- `` +- `` From 5e5ef6e84970ae30974d21eeeff9cd1085a23ab0 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Wed, 23 Jul 2025 10:40:37 -0400 Subject: [PATCH 024/136] update proposal from content tuple --- proposal.md | 254 +++++++++++++++------------------------------------- 1 file changed, 72 insertions(+), 182 deletions(-) diff --git a/proposal.md b/proposal.md index 0a9598ba38..b6e7509720 100644 --- a/proposal.md +++ b/proposal.md @@ -1,19 +1,37 @@ # Top Level Links -- Home (home) - Getting Started (checkmark-circle) -- Examples (code-bracket) -- API Reference (globe) - - SDK Reference - - HTTP API Reference +- Guides (book) +- Examples (plus-circle) +- Reference (globe) +- SDK Reference + - API Reference - UI Components -## Home [guides] +## Getting Started [getting-started] + +### Quickstart + +- App Router +- Pages Router + +### Clerk Dashboard + +- Setting up your Clerk account +- Configure your application -### Getting Started +### Next Steps + +- Custom sign-in/up pages +- Protect specific routes +- Read user and session data +- Add middleware -- App Router Quickstart -- Pages Router Quickstart +### More + +- View all guides + +## Guides [guides] ### Configuring your App [configure] @@ -29,20 +47,21 @@ ### Managing Users [users] - Managing Users [managing] +- Reading Clerk Data [reading] - Extending Clerk Data [extending] - Syncing Clerk Data [syncing] -- Invitations -- User Impersonation +- Impersonation ### Customizing Clerk -- UI Customization [appearance-prop] +- UI Customization (Appearance Prop) [appearance-prop] +- Account Portal - Adding items to UI Components [adding-items] - Email and SMS Templates -- Localization - i18n [localization] -- Clerk Elements - `BETA` [elements] +- Localization (i18n) [localization] +- Clerk Elements (beta) [elements] -### B2B / Organizations [b2b] +### B2B - Overview - Managing Organizations [managing-orgs] @@ -58,31 +77,15 @@ ### Securing your App [secure] -- Overview - Restricting Access -- Multifactor Authentication - MFA [mfa] +- Multifactor Authentication (MFA) [mfa] - Bot Detection - Banning Users - Prevent brute force attacks -- Re-verification / Step-up +- Re-verification (Step-up) - Legal Compliance -- Session Options - Security Best Practices [best-practices] - - Overview - - Security at Clerk - - Vulnerability disclosure policy - - Clerk telemetry - - CSRF protection - - CSP headers - - Fixation protection - - Password protection and rules - -### Clerk Dashboard - -- Overview -- Account Portal -- DNS & Domains -- Plans & Billing +- Session Options ### Development @@ -103,167 +106,54 @@ - Progressive Sign Ups - Troubleshooting -### Core Concepts - -- Overview -- How Clerk Works -- Integrate Clerk -- Clerk Objects -- Security at Clerk [security] -- System Limits -- AI Prompt Library [ai-prompts] -- Multi-tenant Architecture - -## Getting Started [getting-started] - -### Quickstart - -- App Router -- Pages Router - -### Next Steps - -- Custom sign-in/up pages -- Protect specific routes -- Read user and session data -- Add middleware - ### Clerk Dashboard -- Setting up your Clerk account -- Configure your application - -### Learn More - -- View all guides (book) - -## Examples [examples] - -### Integration Type - -- UI Components (box) -- Hooks (plug) - -### Sign In - -- Sign in wth Email & Password -- Sign In with OAuth -- Sign in with Email Code -- Sign in with Email Link -- Sign in with Passkeys -- Multi-Factor Authentication - -## API Reference - -### App Router - -- `auth()` -- `currentUser()` -- Route Handlers -- Server Actions - -### Pages Router - -- `getAuth()` -- `buildClerkProps()` - -### Hooks - -- `useUser()` -- `useClerk()` -- `useAuth()` -- `useSignIn()` -- `useSignUp()` -- `useSession()` -- `useSessionList()` -- `useOrganization()` -- `useOrganizationList()` - -## SDK Reference - -### App Router - -- `auth()` -- `currentUser()` -- Route Handlers -- Server Actions - -### Pages Router - -- `getAuth()` -- `buildClerkProps()` - -### Hooks - -- `useUser()` -- `useClerk()` -- `useAuth()` -- `useSignIn()` -- `useSignUp()` -- `useSession()` -- `useSessionList()` -- `useOrganization()` -- `useOrganizationList()` - -## HTTP API Reference - -### API Reference - - Overview -- Backend API -- Frontend API -- Management API - -## UI Components +- Account Portal +- DNS & Domains +- Plans & Billing -### General +### How Clerk works - Overview -- `` - -### Authentication Components - -- `` -- `` -- `` - -### User Components - -- `` -- `` +- Integrating Clerk +- Cookies +- System limits +- Routing +- Session tokens +- Tokens & signatures +- Clerk environment variables +- Security at Clerk [security] -### Organization Components +## Examples [examples] -- `` -- `` -- `` -- `` +### Testing -### Waitlist Component +- Test Link -- `` +## Reference [reference] -### Billing Components +### General [reference] -- `` +- UI Components (box) [components] -### Control Components +### SDK Reference -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` +- Next.js (nextjs) +- React (react) +- Backend SDK (clerk) +- Node.js (nodejs) +- Go (go) +- Tanstack Start (tanstack) +- Ruby / Rails (ruby) +- Community SDKs + - Vue + - Svelte + - Hono + - Astro -### Unstyled Components +### HTTP API Reference -- `` -- `` -- `` -- `` +- Frontend API [fapi] +- Backend API [bapi] +- Management API [mapi] From a33d50d59c55b0b454718f8dcefe0c64ce25b982 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Wed, 23 Jul 2025 11:18:37 -0400 Subject: [PATCH 025/136] progress checkin: mostly mappings --- proposal-mapping.json | 632 +-------------------- proposal.md | 33 +- scripts/migration-assistant/map-content.ts | 38 +- 3 files changed, 88 insertions(+), 615 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index c8b734f28d..e603135100 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -1,638 +1,46 @@ { - "how-clerk-works/overview.mdx": { - "newPath": "/docs/core-concepts/how-clerk-works", - "action": "consolidate" + "references/**": { + "newPath": "/reference/sdk/**", + "action": "generate" }, - "how-clerk-works/cookies.mdx": { - "newPath": "/docs/core-concepts/how-clerk-works", + "account-portal/**": { + "newPath": "/customizing-clerk/account-portal/**", "action": "consolidate" }, - "security/overview.mdx": { - "newPath": "/docs/core-concepts/security", + "advanced-usage/using-proxies.mdx": { + "newPath": "/dashboard/proxy-fapi", "action": "move" }, - "backend-requests/custom-session-token.mdx": { - "newPath": "/docs/guides/configure/session-token-customization", + "advanced-usage/satellite-domains.mdx": { + "newPath": "/dashboard/satellite-domains", "action": "move" }, - "backend-requests/jwt-templates.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/making-requests.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/manual-jwt.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/overview.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/resources/cookies.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/resources/rate-limits.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/resources/session-tokens.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "backend-requests/resources/tokens-and-signatures.mdx": { - "newPath": "/docs/guides/configure/backend-requests", - "action": "consolidate" - }, - "webhooks/debug-your-webhooks.mdx": { - "newPath": "/docs/guides/configure/syncing-data-with-webhooks", - "action": "consolidate" - }, - "webhooks/inngest.mdx": { - "newPath": "/docs/guides/configure/syncing-data-with-webhooks", - "action": "consolidate" - }, - "webhooks/loops.mdx": { - "newPath": "/docs/guides/configure/syncing-data-with-webhooks", - "action": "consolidate" - }, - "webhooks/overview.mdx": { - "newPath": "/docs/guides/configure/syncing-data-with-webhooks", - "action": "consolidate" - }, - "webhooks/sync-data.mdx": { - "newPath": "/docs/guides/configure/syncing-data-with-webhooks", - "action": "consolidate" - }, - "integrations/analytics/google-analytics.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/convex.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/fauna.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/firebase.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/grafbase.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/hasura.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/instantdb.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/neon.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/nhost.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/databases/supabase.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/overview.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/shopify.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, - "integrations/vercel-marketplace.mdx": { - "newPath": "/docs/guides/configure/integrations", - "action": "consolidate" - }, "ai-prompts/nextjs.mdx": { - "newPath": "/docs/core-concepts/ai-prompts", + "newPath": "/development/ai-prompts", "action": "consolidate" }, "ai-prompts/overview.mdx": { - "newPath": "/docs/core-concepts/ai-prompts", + "newPath": "/development/ai-prompts", "action": "consolidate" }, "ai-prompts/react.mdx": { - "newPath": "/docs/core-concepts/ai-prompts", - "action": "consolidate" - }, - "security/bot-protection.mdx": { - "newPath": "/docs/guides/secure/bot-detection", - "action": "move" - }, - "components/**": { - "newPath": "/docs/reference/components/**", - "action": "generate" - }, - "hooks/**": { - "newPath": "/docs/reference/hooks/**", - "action": "generate" - }, - "references/sdk/**": { - "newPath": "/docs/guides/development/sdk-development", - "action": "consolidate" - }, - "references/**": { - "newPath": "/docs/reference/sdk/**", - "action": "generate" - }, - "billing/b2b-saas.mdx": { - "newPath": "/docs/guides/billing/for-b2b", - "action": "move" - }, - "billing/b2c-saas.mdx": { - "newPath": "/docs/guides/billing/for-b2c", - "action": "move" - }, - "billing/overview.mdx": { - "newPath": "/docs/guides/billing/overview", - "action": "move" - }, - "/docs/examples/testing/test-link": { - "newPath": null, - "action": "drop" - }, - "deployments/migrate-from-cognito.mdx": { - "newPath": "/docs/guides/development/migrating-your-data", - "action": "move" - }, - "deployments/migrate-from-firebase.mdx": { - "newPath": "/docs/guides/development/migrating-your-data", - "action": "move" - }, - "deployments/migrate-overview.mdx": { - "newPath": "/docs/guides/development/migrating-your-data", - "action": "move" - }, - "deployments/clerk-environment-variables.mdx": { - "newPath": "/docs/guides/development/clerk-environment-variables", - "action": "move" - }, - "deployments/**": { - "newPath": "/docs/guides/development/deployment", - "action": "consolidate" - }, - "custom-flows/accept-organization-invitations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/add-email.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/add-phone.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/application-invitations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/bot-sign-up-protection.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/create-organizations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/email-links.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/email-password-mfa.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/email-password.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/email-sms-otp.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/embedded-email-links.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/enterprise-connections.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/error-handling.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/forgot-password.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/google-one-tap.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-membership-requests.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-organization-invitations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-roles.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-sms-based-mfa.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-sso-connections.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-totp-based-mfa.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/manage-user-org-invitations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/multi-session-applications.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/oauth-connections.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/organization-switcher.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/overview.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/passkeys.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/sign-out.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/update-organizations.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "custom-flows/user-impersonation.mdx": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "versioning/available-versions.mdx": { - "newPath": "/docs/guides/development/upgrading/versioning", - "action": "consolidate" - }, - "versioning/overview.mdx": { - "newPath": "/docs/guides/development/upgrading/versioning", - "action": "consolidate" - }, - "errors/overview.mdx": { - "newPath": "/docs/guides/development/troubleshooting", - "action": "move" - }, - "authentication/web3/**": { - "newPath": "/docs/guides/configure/auth-strategies/web3", - "action": "consolidate" - }, - "authentication/social-connections/**": { - "newPath": "/docs/guides/configure/social-connections/**", - "action": "generate" - }, - "organizations/roles-permissions.mdx": { - "newPath": "/docs/guides/b2b/roles-and-permissions", - "action": "consolidate" - }, - "organizations/verified-domains.mdx": { - "newPath": "/docs/guides/b2b/verified-domains", - "action": "move" - }, - "security/clerk-csp.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/csrf-protection.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/email-link-protection.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/fixation-protection.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/password-protection.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/unauthorized-sign-in.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/user-lock-guide.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/vulnerability-disclosure-policy.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "security/xss-leak-protection.mdx": { - "newPath": "/docs/guides/secure/best-practices", - "action": "consolidate" - }, - "testing/cypress/custom-commands.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/cypress/overview.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/cypress/test-account-portal.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/overview.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/playwright/overview.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/playwright/test-authenticated-flows.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/playwright/test-helpers.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/postman-or-insomnia.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "testing/test-emails-and-phones.mdx": { - "newPath": "/docs/guides/development/testing-with-clerk", - "action": "consolidate" - }, - "authentication/enterprise-connections/account-linking.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/authentication-flows.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/easie/google.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/easie/microsoft.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/jit-provisioning.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/oidc/custom-provider.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/overview.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/saml/azure.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/saml/custom-provider.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/saml/google.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "authentication/enterprise-connections/saml/okta.mdx": { - "newPath": "/docs/guides/configure/auth-strategies/enterprise-connections", - "action": "consolidate" - }, - "troubleshooting/create-a-minimal-reproduction.mdx": { - "newPath": "/docs/guides/development/troubleshooting", - "action": "consolidate" - }, - "troubleshooting/email-deliverability.mdx": { - "newPath": "/docs/guides/development/troubleshooting", - "action": "consolidate" - }, - "troubleshooting/overview.mdx": { - "newPath": "/docs/guides/development/troubleshooting", - "action": "consolidate" - }, - "troubleshooting/script-loading.mdx": { - "newPath": "/docs/guides/development/troubleshooting", - "action": "consolidate" - }, - "upgrade-guides/long-term-support.mdx": { - "newPath": "/docs/guides/development/upgrading/versioning", - "action": "consolidate" - }, - "upgrade-guides/sdk-versioning.mdx": { - "newPath": "/docs/guides/development/upgrading/versioning", - "action": "consolidate" - }, - "customization/overview.mdx": { - "newPath": "/docs/guides/customizing-clerk/appearance-prop", - "action": "consolidate" - }, - "customization/themes.mdx": { - "newPath": "/docs/guides/customizing-clerk/appearance-prop", - "action": "consolidate" - }, - "customization/user-button.mdx": { - "newPath": "/docs/guides/customizing-clerk/appearance-prop", - "action": "consolidate" - }, - "customization/user-profile.mdx": { - "newPath": "/docs/guides/customizing-clerk/appearance-prop", - "action": "consolidate" - }, - "customization/variables.mdx": { - "newPath": "/docs/guides/customizing-clerk/appearance-prop", - "action": "consolidate" - }, - "organizations/overview.mdx": { - "newPath": "/docs/guides/b2b/overview", - "action": "move" - }, - "organizations/manage-sso.mdx": { - "newPath": "/docs/guides/b2b/managing-orgs", - "action": "consolidate" - }, - "organizations/invitations.mdx": { - "newPath": "/docs/guides/b2b/managing-orgs", - "action": "consolidate" - }, - "organizations/create-roles-permissions.mdx": { - "newPath": "/docs/guides/b2b/roles-and-permissions", - "action": "consolidate" - }, - "organizations/creator-role.mdx": { - "newPath": "/docs/guides/b2b/roles-and-permissions", - "action": "consolidate" - }, - "organizations/default-role.mdx": { - "newPath": "/docs/guides/b2b/roles-and-permissions", + "newPath": "/development/ai-prompts", "action": "consolidate" }, "quickstarts/**": { - "newPath": "/docs/getting-started/quickstart/**", - "action": "generate" - }, - "upgrade-guides/expo/v2.mdx": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/expo-v2", + "newPath": "/getting-started/**", "action": "move" }, - "upgrade-guides/node-to-express.mdx": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/node-to-express", + "upgrade-guides/**": { + "newPath": "/upgrading-clerk/**", "action": "move" }, - "upgrade-guides/url-based-session-syncing.mdx": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/url-session-syncing", - "action": "move" + "custom-flows/**": { + "newPath": "/examples/**", + "action": "move-to-examples" }, - "upgrade-guides/core-2/**": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/core-2", - "action": "consolidate" - }, - "customization/elements/**": { - "newPath": "/docs/examples", - "action": "convert-to-example" - }, - "customization/elements/overview.mdx": { - "newPath": "/docs/guides/customizing-clerk/elements", - "action": "move" - }, - "customization/elements/reference/**": { - "newPath": "/docs/reference/components/elements/**", - "action": "generate" - }, - "customization/organization-profile.mdx": { - "newPath": "/docs/guides/customizing-clerk/adding-items", - "action": "move" - }, - "customization/localization.mdx": { - "newPath": "/docs/guides/customizing-clerk/localization", - "action": "move" - }, - "upgrade-guides/progressive-sign-up.mdx": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/progressive-sign-ups", - "action": "move" - }, - "account-portal/**": { - "newPath": "/docs/guides/customizing-clerk/account-portal", - "action": "consolidate" - }, - "authentication/configuration/legal-compliance.mdx": { - "newPath": "/docs/guides/secure/legal-compliance", - "action": "move" - }, - "authentication/configuration/restrictions.mdx": { - "newPath": "/docs/guides/secure/restricting-access", - "action": "move" - }, - "users/user-impersonation.mdx": { - "newPath": "/docs/guides/users/impersonation", - "action": "move" - }, - "users/creating-users.mdx": { - "newPath": "/docs/guides/users/managing", - "action": "consolidate" - }, - "users/deleting-users.mdx": { - "newPath": "/docs/guides/users/managing", - "action": "consolidate" - }, - "users/invitations.mdx": { - "newPath": "/docs/guides/users/managing", - "action": "consolidate" - }, - "users/metadata.mdx": { - "newPath": "/docs/guides/users/managing", - "action": "consolidate" - }, - "users/overview.mdx": { - "newPath": "/docs/guides/users/managing", - "action": "consolidate" - }, - "upgrade-guides/nextjs/v6.mdx": { - "newPath": "/docs/guides/development/upgrading/upgrade-guides/next-v6", - "action": "move" - }, - "authentication/configuration/email-sms-templates.mdx": { - "newPath": "/docs/guides/customizing-clerk/email-and-sms-templates", - "action": "move" - }, - "advanced-usage/satellite-domains.mdx": { - "newPath": "/docs/guides/clerk-dashboard/dns-domains", - "action": "consolidate" - }, - "upgrade-guides/overview.mdx": { - "newPath": "/docs/guides/development/upgrading", - "action": "move" - }, - "guides/reverification.mdx": { - "newPath": "/docs/guides/secure/re-verification-step-up", - "action": "move" - }, - "advanced-usage/using-proxies.mdx": { - "newPath": "/docs/guides/clerk-dashboard/dns-domains", - "action": "consolidate" - }, - "authentication/configuration/session-options.mdx": { - "newPath": "/docs/guides/secure/session-options", - "action": "move" - }, - "guides/multi-tenant-architecture.mdx": { - "newPath": "/docs/core-concepts/multi-tenant-architecture", + "authentication/social-connections/**": { + "newPath": "/configure/**", "action": "move" - }, - "authentication/configuration/force-mfa.mdx": { - "newPath": "/docs/guides/secure/mfa", - "action": "consolidate" } } diff --git a/proposal.md b/proposal.md index b6e7509720..3d020a2a9b 100644 --- a/proposal.md +++ b/proposal.md @@ -37,8 +37,35 @@ - Authentication Strategies [auth-strategies] - Social Connections + - Apple + - Atlassian + - Bitbucket + - Box + - Coinbase + - Discord + - Dropbox + - Facebook + - GitHub + - GitLab + - Google + - HubSpot + - Hugging Face + - Line + - Linear + - LinkedIn + - LinkedIn (deprecated) + - Microsoft + - Notion + - Slack + - Spotify + - TikTok + - Twitch + - Twitter v1 (deprecated) + - X/Twitter v2 + - Xero - Enterprise Connections - Web3 + - Session Token customization - Syncing data with webhooks - Backend Requests @@ -105,12 +132,16 @@ - URL based session syncing [url-session-syncing] - Progressive Sign Ups - Troubleshooting +- AI Prompts -### Clerk Dashboard +### Clerk Dashboard [dashboard] - Overview - Account Portal - DNS & Domains + - Overview + - Satellite Domains + - Proxy Clerk Frontend API [proxy-fapi] - Plans & Billing ### How Clerk works diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index d890e441a5..00b7d9f902 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -355,6 +355,40 @@ function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) type InvalidMapping = Awaited> +/** + * Display only unhandled files + * @param {string[]} unhandledFiles - Array of unhandled file paths + */ +function displayUnhandledFilesOnly(unhandledFiles: string[]) { + if (unhandledFiles.length === 0) { + console.log('βœ… All legacy files are handled') + return + } + + console.log(`⚠️ Found ${unhandledFiles.length} unhandled legacy files:\n`) + + // Group by directory for better readability + const groupedFiles: Record = {} + unhandledFiles.forEach((file) => { + const dir = file.includes('/') ? file.split('/')[0] : 'root' + if (!groupedFiles[dir]) groupedFiles[dir] = [] + groupedFiles[dir].push(file) + }) + + // Display grouped warnings + Object.entries(groupedFiles) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([dir, files]) => { + console.log(`πŸ“ ${dir}/ (${files.length} files)`) + files.sort().forEach((file) => { + console.log(` β€’ ${file}`) + }) + console.log() + }) + + console.log(`πŸ“Š Summary: ${unhandledFiles.length} unhandled files`) +} + /** * Main function to parse docs content and check mappings */ @@ -378,8 +412,8 @@ async function main() { const invalidMappings = findInvalidMappings(expandedMapping, manifestPaths) if (WARNINGS_ONLY) { - console.log('πŸ” Checking for unhandled legacy files and pages to create...\n') - displayWarnings(unhandledFiles, pagesToCreate, expandedMapping, invalidMappings) + console.log('πŸ” Checking for unhandled legacy files...\n') + displayUnhandledFilesOnly(unhandledFiles) } else { // Simplified output mode console.log('πŸ“‹ UNHANDLED LEGACY FILES:\n') From fe30e22105f64a27638f9df75d26b9392a3f78d7 Mon Sep 17 00:00:00 2001 From: Kyle MacDonald Date: Wed, 23 Jul 2025 11:26:02 -0400 Subject: [PATCH 026/136] triage webhooks section --- proposal-mapping.json | 14 ++++++++++++++ proposal.md | 6 ++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index e603135100..3f8405fe57 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -42,5 +42,19 @@ "authentication/social-connections/**": { "newPath": "/configure/**", "action": "move" + }, + "webhooks/debug-your-webhooks.mdx": { + "newPath": "/configure/webhooks/debugging", + "action": "move" + }, + "webhooks/inngest.mdx": { + "action": "TODO" + }, + "webhooks/loops.mdx": { + "action": "TODO" + }, + "webhooks/sync-data.mdx": { + "newPath": "/configure/webhooks/syncing", + "action": "move" } } diff --git a/proposal.md b/proposal.md index 3d020a2a9b..85796aa6b2 100644 --- a/proposal.md +++ b/proposal.md @@ -65,9 +65,11 @@ - Xero - Enterprise Connections - Web3 - - Session Token customization -- Syncing data with webhooks +- Webhooks + - Overview + - Syncing data with webhooks [syncing] + - Debugging webhooks [debugging] - Backend Requests - Integrations From 7d67070da36a9b66d08026eabbcf90290953e559 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:47:16 -0400 Subject: [PATCH 027/136] update --- proposal-mapping.json | 436 +++++++++++++++++++++++++++++++++++++++++- proposal.md | 93 ++++++++- 2 files changed, 515 insertions(+), 14 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index 3f8405fe57..6d74f56a29 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -1,8 +1,20 @@ { + "index.mdx": { + "newPath": "index.mdx", + "action": "move" + }, "references/**": { "newPath": "/reference/sdk/**", "action": "generate" }, + "hooks/**": { + "newPath": "/reference/hooks/**", + "action": "generate" + }, + "components/**": { + "newPath": "/reference/components/**", + "action": "move" + }, "account-portal/**": { "newPath": "/customizing-clerk/account-portal/**", "action": "consolidate" @@ -32,15 +44,39 @@ "action": "move" }, "upgrade-guides/**": { - "newPath": "/upgrading-clerk/**", + "newPath": "/development/upgrading/**", "action": "move" }, "custom-flows/**": { "newPath": "/examples/**", "action": "move-to-examples" }, + "authentication/overview.mdx": { + "newPath": "/configure/overview", + "action": "move" + }, "authentication/social-connections/**": { - "newPath": "/configure/**", + "newPath": "/configure/auth-strategies/social-connections/**", + "action": "move" + }, + "authentication/enterprise-connections/**": { + "newPath": "/configure/auth-strategies/enterprise-connections/**", + "action": "move" + }, + "authentication/web3/**": { + "newPath": "/configure/auth-strategies/web3/**", + "action": "move" + }, + "backend-requests/custom-session-token.mdx": { + "newPath": "/configure/session-token", + "action": "move" + }, + "webhooks/overview.mdx": { + "newPath": "/configure/webhooks/overview", + "action": "move" + }, + "webhooks/sync-data.mdx": { + "newPath": "/configure/webhooks/syncing", "action": "move" }, "webhooks/debug-your-webhooks.mdx": { @@ -53,8 +89,400 @@ "webhooks/loops.mdx": { "action": "TODO" }, - "webhooks/sync-data.mdx": { - "newPath": "/configure/webhooks/syncing", + "integrations/**": { + "newPath": "/configure/integrations/**", + "action": "move" + }, + "authentication/configuration/email-sms-templates.mdx": { + "newPath": "/customizing-clerk/email-sms-templates", + "action": "move" + }, + "authentication/configuration/legal-compliance.mdx": { + "newPath": "/secure/legal-compliance", + "action": "move" + }, + "authentication/configuration/restrictions.mdx": { + "newPath": "/secure/restricting-access", + "action": "move" + }, + "authentication/configuration/session-options.mdx": { + "newPath": "/secure/session-options", + "action": "move" + }, + "authentication/configuration/sign-up-sign-in-options.mdx": { + "newPath": "/configure/auth-strategies/sign-up-sign-in-options", + "action": "move" + }, + "authentication/configuration/force-mfa.mdx": { + "action": "deleted" + }, + "backend-requests/jwt-templates.mdx": { + "newPath": "/development/jwt-templates", + "action": "move" + }, + "backend-requests/overview.mdx": { + "newPath": "/development/making-requests", + "action": "consolidate" + }, + "backend-requests/making-requests.mdx": { + "newPath": "/development/making-requests", + "action": "move" + }, + "backend-requests/manual-jwt.mdx": { + "newPath": "/development/manual-jwt", + "action": "move" + }, + "backend-requests/resources/cookies.mdx": { + "newPath": "/how-clerk-works/cookies", + "action": "consolidate" + }, + "backend-requests/resources/rate-limits.mdx": { + "newPath": "/how-clerk-works/rate-limits", + "action": "move" + }, + "backend-requests/resources/session-tokens.mdx": { + "newPath": "/how-clerk-works/session-tokens", + "action": "move" + }, + "backend-requests/resources/tokens-and-signatures.mdx": { + "newPath": "/how-clerk-works/tokens-and-signatures", + "action": "move" + }, + "billing/overview.mdx": { + "newPath": "/billing/overview", + "action": "move" + }, + "billing/b2b-saas.mdx": { + "newPath": "/billing/for-b2b", + "action": "move" + }, + "billing/b2c-saas.mdx": { + "newPath": "/billing/for-b2c", + "action": "move" + }, + "customization/overview.mdx": { + "newPath": "/customizing-clerk/appearance-prop/overview", + "action": "move" + }, + "customization/layout.mdx": { + "newPath": "/customizing-clerk/appearance-prop/layout", + "action": "move" + }, + "customization/themes.mdx": { + "newPath": "/customizing-clerk/appearance-prop/themes", + "action": "move" + }, + "customization/variables.mdx": { + "newPath": "/customizing-clerk/appearance-prop/variables", + "action": "move" + }, + "customization/captcha.mdx": { + "newPath": "/customizing-clerk/appearance-prop/captcha", + "action": "move" + }, + "customization/organization-profile.mdx": { + "newPath": "/customizing-clerk/adding-items/organization-profile", + "action": "move" + }, + "customization/user-button.mdx": { + "newPath": "/customizing-clerk/adding-items/user-button", + "action": "move" + }, + "customization/user-profile.mdx": { + "newPath": "/customizing-clerk/adding-items/user-profile", + "action": "move" + }, + "customization/localization.mdx": { + "newPath": "/customizing-clerk/localization", + "action": "move" + }, + "customization/elements/**": { + "newPath": "/customizing-clerk/elements/**", + "action": "move" + }, + "deployments/environments.mdx": { + "newPath": "/development/deployment/environments", + "action": "move" + }, + "deployments/clerk-environment-variables.mdx": { + "newPath": "/development/deployment/clerk-environment-variables", + "action": "move" + }, + "deployments/changing-domains.mdx": { + "newPath": "/development/deployment/changing-domains", + "action": "move" + }, + "deployments/overview.mdx": { + "newPath": "/development/deployment/production", + "action": "move" + }, + "deployments/deploy-to-vercel.mdx": { + "newPath": "/development/deployment/deploy-to-vercel", + "action": "move" + }, + "deployments/deploy-behind-a-proxy.mdx": { + "newPath": "/development/deployment/deploy-behind-a-proxy", + "action": "move" + }, + "deployments/deploy-astro.mdx": { + "newPath": "/development/deployment/deploy-astro", + "action": "move" + }, + "deployments/deploy-chrome-extension.mdx": { + "newPath": "/development/deployment/deploy-chrome-extension", + "action": "move" + }, + "deployments/deploy-expo.mdx": { + "newPath": "/development/deployment/deploy-expo", + "action": "move" + }, + "deployments/set-up-preview-environment.mdx": { + "newPath": "/development/deployment/set-up-preview-environment", + "action": "move" + }, + "deployments/set-up-staging.mdx": { + "newPath": "/development/deployment/set-up-staging", + "action": "move" + }, + "deployments/staging-alternatives.mdx": { + "newPath": "/development/deployment/staging-alternatives", + "action": "move" + }, + "deployments/migrate-overview.mdx": { + "newPath": "/development/migrating/overview", + "action": "move" + }, + "deployments/exporting-users.mdx": { + "newPath": "/development/migrating/overview", + "action": "consolidate" + }, + "deployments/migrate-from-cognito.mdx": { + "newPath": "/development/migrating/from-cognito", + "action": "move" + }, + "deployments/migrate-from-firebase.mdx": { + "newPath": "/development/migrating/from-firebase", + "action": "move" + }, + "errors/overview.mdx": { + "newPath": "/development/errors/overview", + "action": "move" + }, + "guides/authorization-checks.mdx": { + "newPath": "/secure/authorization-checks", + "action": "move" + }, + "guides/custom-redirects.mdx": { + "newPath": "/development/custom-redirects", + "action": "move" + }, + "guides/custom-types.mdx": { + "newPath": "/development/custom-types", + "action": "move" + }, + "guides/image-optimization.mdx": { + "newPath": "/development/image-optimization", + "action": "move" + }, + "guides/multi-tenant-architecture.mdx": { + "newPath": "/how-clerk-works/multi-tenant-architecture", + "action": "move" + }, + "guides/overview.mdx": { + "action": "delete" + }, + "guides/reverification.mdx": { + "newPath": "/secure/reverification", + "action": "move" + }, + "guides/routing.mdx": { + "newPath": "/how-clerk-works/routing", + "action": "move" + }, + "how-clerk-works/cookies.mdx": { + "newPath": "/how-clerk-works/cookies", + "action": "move" + }, + "how-clerk-works/overview.mdx": { + "newPath": "/how-clerk-works/overview", + "action": "move" + }, + "oauth/**": { + "newPath": "/configure/auth-strategies/oauth/**", + "action": "move" + }, + "organizations/create-orgs-for-users.mdx": { + "newPath": "/organizations/create-orgs-for-users", + "action": "move" + }, + "organizations/force-organizations.mdx": { + "action": "deleted" + }, + "organizations/invitations.mdx": { + "newPath": "/organizations/invitations", + "action": "move" + }, + "organizations/manage-sso.mdx": { + "newPath": "/organizations/manage-sso", + "action": "move" + }, + "organizations/metadata.mdx": { + "newPath": "/organizations/metadata", + "action": "move" + }, + "organizations/org-slugs-in-urls.mdx": { + "newPath": "/organizations/org-slugs-in-urls", + "action": "move" + }, + "organizations/organization-workspaces.mdx": { + "newPath": "/dashboard/overview", + "action": "consolidate" + }, + "organizations/overview.mdx": { + "newPath": "/organizations/overview", + "action": "move" + }, + "organizations/roles-permissions.mdx": { + "newPath": "/organizations/roles-permissions", + "action": "move" + }, + "organizations/verified-domains.mdx": { + "newPath": "/organizations/verified-domains", + "action": "move" + }, + "testing/cypress/custom-commands.mdx": { + "newPath": "/development/testing/cypress/custom-commands", + "action": "move" + }, + "testing/cypress/overview.mdx": { + "newPath": "/development/testing/cypress/overview", + "action": "move" + }, + "testing/cypress/test-account-portal.mdx": { + "newPath": "/development/testing/cypress/test-account-portal", + "action": "move" + }, + "testing/overview.mdx": { + "newPath": "/development/testing/overview", + "action": "move" + }, + "testing/playwright/overview.mdx": { + "newPath": "/development/testing/playwright/overview", + "action": "move" + }, + "testing/playwright/test-authenticated-flows.mdx": { + "newPath": "/development/testing/playwright/test-authenticated-flows", + "action": "move" + }, + "testing/playwright/test-helpers.mdx": { + "newPath": "/development/testing/playwright/test-helpers", + "action": "move" + }, + "testing/postman-or-insomnia.mdx": { + "newPath": "/development/testing/postman-or-insomnia", + "action": "move" + }, + "testing/test-emails-and-phones.mdx": { + "newPath": "/development/testing/test-emails-and-phones", + "action": "move" + }, + "troubleshooting/create-a-minimal-reproduction.mdx": { + "newPath": "/development/troubleshooting/create-a-minimal-reproduction", + "action": "move" + }, + "troubleshooting/email-deliverability.mdx": { + "newPath": "/development/troubleshooting/email-deliverability", + "action": "move" + }, + "troubleshooting/overview.mdx": { + "newPath": "/development/troubleshooting/overview", + "action": "move" + }, + "troubleshooting/script-loading.mdx": { + "newPath": "/development/troubleshooting/script-loading", + "action": "move" + }, + "users/overview.mdx": { + "newPath": "/users/managing", + "action": "move" + }, + "users/creating-users.mdx": { + "newPath": "/users/managing", + "action": "consolidate" + }, + "users/deleting-users.mdx": { + "newPath": "/users/managing", + "action": "move" + }, + "users/invitations.mdx": { + "newPath": "/users/managing", + "action": "consolidate" + }, + "users/metadata.mdx": { + "newPath": "/users/managing", + "action": "consolidate" + }, + "users/user-impersonation.mdx": { + "newPath": "/users/impersonation", + "action": "move" + }, + "security/bot-protection.mdx": { + "newPath": "/secure/bot-protection", "action": "move" + }, + "security/csrf-protection.mdx": { + "newPath": "/secure/best-practices/csrf-protection", + "action": "move" + }, + "security/clerk-csp.mdx": { + "newPath": "/secure/best-practices/csp-headers", + "action": "move" + }, + "security/email-link-protection.mdx": { + "newPath": "/secure/best-practices/email-link-protection", + "action": "move" + }, + "security/xss-leak-protection.mdx": { + "newPath": "/secure/best-practices/xss-leak-protection", + "action": "move" + }, + "security/fixation-protection.mdx": { + "newPath": "/secure/best-practices/fixation-protection", + "action": "move" + }, + "security/vulnerability-disclosure-policy.mdx": { + "newPath": "/security/vulnerability-disclosure-policy", + "action": "move" + }, + "security/password-protection.mdx": { + "newPath": "/security/password-protection", + "action": "move" + }, + "telemetry.mdx": { + "newPath": "/security/telemetry", + "action": "move" + }, + "security/user-lock-guide.mdx": { + "newPath": "/secure/brute-force-attacks", + "action": "move" + }, + "security/unauthorized-sign-in.mdx": { + "newPath": "/secure/unauthorized-sign-in", + "action": "move" + }, + "security/overview.mdx": { + "action": "delete" + }, + "maintenance-mode.mdx": { + "newPath": "maintenance-mode.mdx", + "action": "move" + }, + "versioning/available-versions.mdx": { + "newPath": "/development/versioning", + "action": "consolidate" + }, + "versioning/overview.mdx": { + "newPath": "/development/versioning", + "action": "consolidate" } } diff --git a/proposal.md b/proposal.md index 85796aa6b2..3e7822080b 100644 --- a/proposal.md +++ b/proposal.md @@ -4,7 +4,7 @@ - Guides (book) - Examples (plus-circle) - Reference (globe) -- SDK Reference + - SDK Reference - API Reference - UI Components @@ -36,6 +36,7 @@ ### Configuring your App [configure] - Authentication Strategies [auth-strategies] + - Sign-up and sign-in options - Social Connections - Apple - Atlassian @@ -64,38 +65,82 @@ - X/Twitter v2 - Xero - Enterprise Connections + - Overview + - Authentication flows + - Account linking + - Just-in-Time account provisioning + - SAML providers + - Azure + - Google + - Okta + - Custom provider + - OIDC providers + - Custom provider + - EASIE provider + - Microsoft + - Google - Web3 + - Coinbase Wallet + - Metamask + - OKX Wallet + - OAuth + - What are OAuth & OIDC + - How Clerk implements OAuth + - Use OAuth for Single Sign-On (SSO) + - Use OAuth for scoped access + - Verify OAuth tokens - Session Token customization - Webhooks - Overview - Syncing data with webhooks [syncing] - Debugging webhooks [debugging] -- Backend Requests - Integrations + - Overview + - Databases + - Convex + - Fauna + - Firebase + - Grafbase + - Hasura + - InstantDB + - Nhost + - Supabase + - Neon + - Platforms + - Shopify + - Vercel Marketplace + - Analytics + - Google Analytics ### Managing Users [users] - Managing Users [managing] - Reading Clerk Data [reading] - Extending Clerk Data [extending] -- Syncing Clerk Data [syncing] - Impersonation ### Customizing Clerk - UI Customization (Appearance Prop) [appearance-prop] + - Overview + - Layout + - Theme + - Variables + - CAPTCHA - Account Portal - Adding items to UI Components [adding-items] - Email and SMS Templates - Localization (i18n) [localization] - Clerk Elements (beta) [elements] -### B2B +### B2B (Organizations) [organizations] - Overview - Managing Organizations [managing-orgs] - Verified Domains - Roles and Permissions +- Invitations +- Metadata - SSO / Enterprise Connections ### Billing @@ -108,21 +153,48 @@ - Restricting Access - Multifactor Authentication (MFA) [mfa] +- Authorization Checks - Bot Detection - Banning Users - Prevent brute force attacks - Re-verification (Step-up) - Legal Compliance +- Password protection and rules - Security Best Practices [best-practices] + - XSS leak protection + - CSRF protection + - CSP Headers + - Fixation protection + - Brute force attacks and locking user accounts + - Protect sign ups from bots + - Protect email link sign-ins and sign-ups + - Unauthorized sign-in - Session Options ### Development -- Deployment -- Testing with Clerk - Managing Environments -- Migrating your Data -- Clerk environment variables +- Clerk Environment Variables +- Customize Redirect URLs +- Override Clerk Types/Interfaces +- Image Optimization +- Testing with Clerk [testing] +- Errors +- Troubleshooting +- Deployment + - Changing domains + - Deploy to production + - Deploy to Vercel + - Deploy behind a proxy + - Deploy an Astro app to production + - Deploy a Chrome Extension to production + - Deploy an Expo app to production + - Set up a staging environment + - Set up a preview environment +- Migrating your Data [migrating] + - Overview + - Migrate from Firebase + - Migrate from Cognito - SDK Development - Upgrading Clerk [upgrading] - Versioning & LTS [versioning] @@ -133,13 +205,11 @@ - @clerk/nextjs v6 [next-v6] - URL based session syncing [url-session-syncing] - Progressive Sign Ups -- Troubleshooting - AI Prompts ### Clerk Dashboard [dashboard] - Overview -- Account Portal - DNS & Domains - Overview - Satellite Domains @@ -157,6 +227,9 @@ - Tokens & signatures - Clerk environment variables - Security at Clerk [security] + - Vulnerability disclosure policy + - Clerk Telemetry +- Multi-tenant architecture ## Examples [examples] From cc9887572bd5e5d67450a57009fd53f1ad7185df Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 26 Jul 2025 02:25:38 +0800 Subject: [PATCH 028/136] work on the mapping script --- package.json | 3 +- proposal-mapping.json | 4 +- scripts/migration-assistant/map-content.ts | 412 +++++++++++++++++---- scripts/move-doc.mjs | 149 +++++++- 4 files changed, 490 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 3692928ef7..11ba384473 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "move-doc": "node scripts/move-doc.mjs", "migration:generate-manifest": "tsx scripts/migration-assistant/generate-manifest.ts", "migration:generate-manifest:watch": "tsx watch scripts/migration-assistant/generate-manifest.ts --watch", - "migration:map-content": "tsx scripts/migration-assistant/map-content.ts" + "migration:map-content": "tsx scripts/migration-assistant/map-content.ts", + "migration:map-content:dev": "tsx watch scripts/migration-assistant/map-content.ts" }, "devDependencies": { "@parcel/watcher": "^2.5.1", diff --git a/proposal-mapping.json b/proposal-mapping.json index 6d74f56a29..872893b8f3 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -5,11 +5,11 @@ }, "references/**": { "newPath": "/reference/sdk/**", - "action": "generate" + "action": "move" }, "hooks/**": { "newPath": "/reference/hooks/**", - "action": "generate" + "action": "move" }, "components/**": { "newPath": "/reference/components/**", diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 00b7d9f902..7a56ebf3db 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -1,5 +1,5 @@ import 'dotenv/config' -import { readFile } from 'fs/promises' +import { readFile, access } from 'fs/promises' import { glob } from 'glob' import { join, relative } from 'path' import type { Manifest } from './generate-manifest' @@ -22,7 +22,19 @@ async function readMapping() { const mappingContent = await readFile(MAPPING_PATH, 'utf-8') return JSON.parse(mappingContent) as Record< string, - { newPath: string; action: 'consolidate' | 'move' | 'generate' | 'convert-to-example' | 'drop' } + { + newPath: string + action: + | 'consolidate' + | 'move' + | 'generate' + | 'convert-to-example' + | 'move-to-examples' + | 'drop' + | 'delete' + | 'deleted' + | 'TODO' + } > } catch (error) { console.error(`❌ Error reading mapping.json: ${error.message}`) @@ -331,7 +343,7 @@ function expandGlobMapping(mapping: Mapping, allFiles: string[]) { */ function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) { const manifestPathsSet = new Set(manifestPaths) - const invalidMappings: Array<{ source: string; destination: string; action: 'consolidate' | 'move' }> = [] + const invalidMappings: Array<{ source: string; destination: string; action: string }> = [] for (const [sourcePath, mapping] of Object.entries(expandedMapping)) { // Skip drop actions - they're intentionally removing paths @@ -390,83 +402,353 @@ function displayUnhandledFilesOnly(unhandledFiles: string[]) { } /** - * Main function to parse docs content and check mappings + * Check if a file exists */ -async function main() { - if (!WARNINGS_ONLY) { - console.log('πŸ” Scanning for MDX files in ./docs') +async function fileExists(filePath: string): Promise { + try { + await access(filePath) + return true + } catch { + return false } +} - try { - const [mdxFiles, mapping, { paths: manifestPaths }] = await Promise.all([ - findMdxFiles(DOCS_PATH), - readMapping(), - readProposalManifestPaths(), - ]) +/** + * Get icon for action type + */ +function getActionIcon(action: string): string { + const icons = { + move: 'πŸ“', + consolidate: 'πŸ”€', + generate: '✨', + 'convert-to-example': 'πŸ“„', + 'move-to-examples': 'πŸ“š', + delete: 'πŸ—‘οΈ', + deleted: '❌', + drop: '⬇️', + TODO: '❓', + } + return icons[action] || 'β€’' +} - // Expand glob patterns in mapping to actual file matches - const expandedMapping = expandGlobMapping(mapping, mdxFiles) +/** + * Find common patterns in move tasks that can be consolidated with globs + */ +function consolidateMoveTasks(moveTasks: Array<{ source: string; destination: string }>) { + const patterns: Array<{ + pattern: string + destPattern: string + files: Array<{ source: string; destination: string }> + command: string + }> = [] + const individual: Array<{ source: string; destination: string }> = [] + + // Group by common directory patterns + const groups: Record> = {} + + moveTasks.forEach((task) => { + // Find the common directory pattern + const sourceParts = task.source.split('/') + const destParts = task.destination + .replace(/^\/docs\//, '') + .split('/') + .filter((p) => p) // Remove empty parts + + // Look for directory-level patterns (at least 2 files in same directory) + if (sourceParts.length >= 2) { + const baseDir = sourceParts.slice(0, -1).join('/') + const destBaseDir = destParts.slice(0, -1).join('/') + const groupKey = `${baseDir} β†’ ${destBaseDir}` + + if (!groups[groupKey]) { + groups[groupKey] = [] + } + groups[groupKey].push(task) + } + }) - const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) - const pagesToCreate = findPagesToCreate(manifestPaths, expandedMapping) - const invalidMappings = findInvalidMappings(expandedMapping, manifestPaths) + // Convert groups with 2+ files into glob patterns + Object.entries(groups).forEach(([groupKey, files]) => { + if (files.length >= 2) { + const [sourceBase, destBase] = groupKey.split(' β†’ ') + + // Check if all files in this group follow the same pattern + const allSamePattern = files.every((file) => { + const expectedSource = `${sourceBase}/${file.source.split('/').pop()}` + const destWithoutDocs = file.destination.replace(/^\/docs\//, '').replace(/^\//, '') + const expectedDest = `${destBase}/${destWithoutDocs.split('/').pop()}` + return file.source === expectedSource && destWithoutDocs === expectedDest + }) - if (WARNINGS_ONLY) { - console.log('πŸ” Checking for unhandled legacy files...\n') - displayUnhandledFilesOnly(unhandledFiles) - } else { - // Simplified output mode - console.log('πŸ“‹ UNHANDLED LEGACY FILES:\n') - if (unhandledFiles.length > 0) { - unhandledFiles.sort().forEach((file) => { - console.log(` β€’ ${file}`) + if (allSamePattern) { + patterns.push({ + pattern: `/docs/${sourceBase}/*`, + destPattern: `/docs/${destBase}/*`, + files, + command: `node scripts/move-doc.mjs "/docs/${sourceBase}/*" "/docs/${destBase}/*"`, }) } else { - console.log(' βœ… All legacy files are handled') + // If not exact pattern match, add to individual + individual.push(...files) } + } else { + // Single files go to individual + individual.push(...files) + } + }) - console.log('\n❌ INVALID MAPPINGS (destinations not in manifests):\n') - if (invalidMappings.length > 0) { - invalidMappings.forEach((mapping) => { - console.log(` β€’ ${mapping.source} β†’ ${mapping.destination} (${mapping.action})`) - }) - } else { - console.log(' βœ… All mappings point to valid manifest paths') - } + return { patterns, individual } +} + +/** + * Group consolidation tasks by destination + */ +function groupConsolidationTasks(consolidateTasks: Array<{ source: string; destination: string }>) { + const groups: Record> = {} + + consolidateTasks.forEach((task) => { + if (!groups[task.destination]) { + groups[task.destination] = [] + } + groups[task.destination].push(task.source) + }) + + return Object.entries(groups).map(([destination, sources]) => ({ + destination, + sources: sources.sort(), + count: sources.length, + })) +} + +/** + * Display actions needed for each mapping item + */ +async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[]) { + // Collect all tasks, filtering out completed tasks + const allTasks: Array<{ source: string; destination: string; action: string }> = [] + const moveTasks: Array<{ source: string; destination: string }> = [] + const consolidateTasks: Array<{ source: string; destination: string }> = [] + let skippedCount = 0 + + for (const [source, mapping] of Object.entries(expandedMapping)) { + const sourcePath = join(DOCS_PATH, source) + + // Skip checking file existence for actions without destination paths + if ( + !mapping.newPath || + mapping.action === 'delete' || + mapping.action === 'deleted' || + mapping.action === 'drop' || + mapping.action === 'TODO' + ) { + allTasks.push({ source, destination: mapping.newPath || '', action: mapping.action }) + continue + } + + // Construct destination path - remove leading slash and add .mdx extension + let destinationRelativePath = mapping.newPath.replace(/^\//, '') + if (!destinationRelativePath.endsWith('.mdx')) { + destinationRelativePath += '.mdx' + } + const destinationPath = join(DOCS_PATH, destinationRelativePath) + + // Check if task is already completed (source doesn't exist but destination does) + const sourceExists = await fileExists(sourcePath) + const destinationExists = await fileExists(destinationPath) + + if (!sourceExists && destinationExists) { + skippedCount++ + continue // Skip this task as it's already completed + } + + if (mapping.action === 'move') { + moveTasks.push({ source, destination: mapping.newPath }) + } else if (mapping.action === 'consolidate') { + consolidateTasks.push({ source, destination: mapping.newPath }) + } else { + allTasks.push({ source, destination: mapping.newPath, action: mapping.action }) + } + } + + console.log('πŸ“‹ PROPOSAL MAPPING TASKS:\n') + + // Consolidate move tasks into patterns + const { patterns, individual: individualMoves } = consolidateMoveTasks(moveTasks) + + // Group consolidation tasks by destination + const consolidationGroups = groupConsolidationTasks(consolidateTasks) + + let taskNumber = 1 - console.log('\nπŸ“ MANIFEST PATHS:\n') - manifestPaths.forEach((path) => { - console.log(` β€’ ${path}`) + // Show consolidated move patterns first + if (patterns.length > 0) { + console.log('πŸš€ BATCH MOVE COMMANDS (using glob patterns):\n') + + patterns.forEach((pattern) => { + const paddedNumber = taskNumber.toString().padStart(3, '0') + console.log( + `${paddedNumber}. πŸ“ [BATCH MOVE] ${pattern.files.length} files: ${pattern.pattern} β†’ ${pattern.destPattern}`, + ) + console.log(` ${pattern.command}`) + console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) + console.log('') + taskNumber++ + }) + + console.log('─'.repeat(80) + '\n') + } + + // Show consolidation tasks grouped by destination + if (consolidationGroups.length > 0) { + console.log('πŸ”€ CONSOLIDATION TASKS (grouped by destination):\n') + + consolidationGroups.forEach((group) => { + const paddedNumber = taskNumber.toString().padStart(3, '0') + console.log(`${paddedNumber}. πŸ”€ [CONSOLIDATE] Create guide: ${group.destination}`) + console.log(` Consolidate content from ${group.count} files:`) + group.sources.forEach((source) => { + console.log(` β€’ ${source}`) }) + console.log('') + taskNumber++ + }) - // Count files that need to be converted to examples - const convertToExampleFiles = Object.values(expandedMapping).filter( - (mapping) => mapping.action === 'convert-to-example', - ).length - - // Count files that will be generated - const generateFiles = Object.values(expandedMapping).filter((mapping) => mapping.action === 'generate').length - - console.log('\nπŸ“Š SUMMARY:') - console.log(` β€’ Legacy files: ${mdxFiles.length}`) - console.log(` β€’ Manifest paths: ${manifestPaths.length}`) - console.log(` β€’ Original mapping entries: ${Object.keys(mapping).length}`) - console.log(` β€’ Expanded mapping entries: ${Object.keys(expandedMapping).length}`) - console.log(` β€’ Handled files: ${Object.keys(expandedMapping).length}`) - console.log(` β€’ Unhandled files: ${unhandledFiles.length}`) - console.log(` β€’ Pages needing new content: ${pagesToCreate.length}`) - console.log(` β€’ Convert to examples: ${convertToExampleFiles}`) - console.log(` β€’ Pages need to be generated: ${generateFiles}`) - console.log(` β€’ Invalid mappings: ${invalidMappings.length}`) - - if (unhandledFiles.length > 0 || pagesToCreate.length > 0) { - console.log('\nπŸ’‘ Run with DOCS_WARNINGS_ONLY=true to see detailed warnings and grouped files') - } + console.log('─'.repeat(80) + '\n') + } + + // Add individual move tasks to allTasks + individualMoves.forEach((move) => { + allTasks.push({ source: move.source, destination: move.destination, action: 'move' }) + }) + + // Sort all remaining tasks by source path + allTasks.sort((a, b) => a.source.localeCompare(b.source)) + + // Display individual tasks + allTasks.forEach((task) => { + const icon = getActionIcon(task.action) + const paddedNumber = taskNumber.toString().padStart(3, '0') + + // Always show the task description first + if (task.action === 'delete' || task.action === 'deleted' || task.action === 'drop') { + console.log(`${paddedNumber}. ${icon} [${task.action.toUpperCase()}] ${task.source}`) + } else if (task.action === 'TODO') { + console.log(`${paddedNumber}. ${icon} [${task.action.toUpperCase()}] ${task.source} β†’ NEEDS MANUAL REVIEW`) + } else { + console.log(`${paddedNumber}. ${icon} [${task.action.toUpperCase()}] ${task.source} β†’ ${task.destination}`) } - } catch (error) { - console.error('❌ Error scanning docs:', error.message) - process.exit(1) + + // Show the executable command if available + if (task.action === 'move') { + const sourcePath = `/docs/${task.source.replace(/\.mdx$/, '')}` + const destPath = task.destination.startsWith('/docs/') + ? task.destination.replace(/\.mdx$/, '') + : `/docs${task.destination.replace(/\.mdx$/, '')}` + console.log(` node scripts/move-doc.mjs ${sourcePath} ${destPath}`) + } else if (task.action === 'delete') { + const filePath = `docs/${task.source}` + console.log(` rm ${filePath}`) + } + + // Add a line break between tasks + console.log('') + taskNumber++ + }) + + // Show summary + const totalMapped = Object.keys(expandedMapping).length + const totalFiles = mdxFiles.length + const unmappedCount = totalFiles - totalMapped + const batchMoveFiles = patterns.reduce((sum, pattern) => sum + pattern.files.length, 0) + const individualMoveFiles = individualMoves.length + const consolidationFiles = consolidateTasks.length + const remainingTasks = patterns.length + consolidationGroups.length + allTasks.length + + console.log(`\nπŸ“Š SUMMARY:`) + console.log(` β€’ Total legacy files: ${totalFiles}`) + console.log(` β€’ Files with mappings: ${totalMapped}`) + console.log(` β€’ Unmapped files: ${unmappedCount}`) + if (skippedCount > 0) { + console.log(` β€’ βœ… Completed tasks (skipped): ${skippedCount}`) + } + console.log( + ` β€’ Remaining tasks: ${remainingTasks} (${patterns.length} batch + ${consolidationGroups.length} consolidation + ${allTasks.length} individual)`, + ) + + // Show action counts + const actionCounts: Record = {} + allTasks.forEach((task) => { + actionCounts[task.action] = (actionCounts[task.action] || 0) + 1 + }) + + // Add batch moves and consolidations to counts + if (patterns.length > 0) { + actionCounts['batch-move'] = patterns.length + actionCounts['move'] = individualMoveFiles // Individual moves only + } + if (consolidationGroups.length > 0) { + actionCounts['consolidate'] = consolidationGroups.length + } + + console.log(`\nπŸ“ˆ ACTION BREAKDOWN:`) + if (patterns.length > 0) { + console.log(` β€’ batch-move: ${patterns.length} commands (${batchMoveFiles} files)`) + } + if (consolidationGroups.length > 0) { + console.log(` β€’ consolidate: ${consolidationGroups.length} guides (${consolidationFiles} source files)`) + } + Object.entries(actionCounts) + .filter(([action]) => action !== 'batch-move' && action !== 'consolidate') // Already shown above + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([action, count]) => { + console.log(` β€’ ${action}: ${count} files`) + }) + + console.log(`\nπŸ’‘ Optimizations:`) + if (patterns.length > 0) { + console.log(` β€’ Batch commands can move ${batchMoveFiles} files with just ${patterns.length} commands!`) + } + if (consolidationGroups.length > 0) { + console.log( + ` β€’ Consolidation creates ${consolidationGroups.length} guides from ${consolidationFiles} source files!`, + ) + } +} + +/** + * Main function to parse docs content and show mapping actions + */ +async function main() { + const [mdxFiles, mapping, { paths: manifestPaths }] = await Promise.all([ + findMdxFiles(DOCS_PATH), + readMapping(), + readProposalManifestPaths(), + ]) + + // Expand glob patterns in mapping to actual file matches + const expandedMapping = expandGlobMapping(mapping, mdxFiles) + + if (WARNINGS_ONLY) { + const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) + console.log('πŸ” Checking for unhandled legacy files...\n') + displayUnhandledFilesOnly(unhandledFiles) + return } + + // Show what actions are needed for each mapping + await displayMappingActions(expandedMapping, mdxFiles) + + // Show unhandled files + const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) + if (unhandledFiles.length > 0) { + console.log(`\n⚠️ UNHANDLED LEGACY FILES (${unhandledFiles.length} files):`) + console.log(` These files exist but have no mapping defined:\n`) + unhandledFiles.sort().forEach((file) => { + console.log(` β€’ ${file}`) + }) + } + + console.log('\nπŸ’‘ Run with DOCS_WARNINGS_ONLY=true to see only unhandled files') } // Run the script diff --git a/scripts/move-doc.mjs b/scripts/move-doc.mjs index e423ae39ac..165f461b7c 100644 --- a/scripts/move-doc.mjs +++ b/scripts/move-doc.mjs @@ -1,25 +1,38 @@ /** - * Relocates an MDX file. + * Relocates MDX files (single file or batch using glob patterns). * * At a high level, the script does the following in the /clerk-docs repo: - * 1. Moves the mdx file to the new location - * 2. Updates the manifest.json file to update all links that point to the old location - * 3. Updates any links in other mdx files that point to the old location - * 4. Adds the redirect to the redirects/static/docs.json file - * 5. Updates any existing redirects to point to the new location + * 1. Moves the mdx file(s) to the new location(s) + * 2. Updates the manifest.json file to update all links that point to the old location(s) + * 3. Updates any links in other mdx files that point to the old location(s) + * 4. Adds the redirect(s) to the redirects/static/docs.json file + * 5. Updates any existing redirects to point to the new location(s) * * The format to run the script is: * node scripts/move-doc.mjs /docs/old-path /docs/new-path * - * @example + * @example Single file move: * node scripts/move-doc.mjs /docs/references/nextjs/overview /docs/references/nextjs/available-methods * - * Note: The .mdx extension should be omitted from the paths as the script will add it + * @example Batch move with glob patterns: + * node scripts/move-doc.mjs "/docs/references/**" "/docs/reference/sdk/**" + * node scripts/move-doc.mjs "/docs/quickstarts/*" "/docs/getting-started/*" + * + * Supported glob patterns: + * - * matches any characters except / + * - ** matches any characters including / + * - ? matches any single character except / + * + * Note: + * - The .mdx extension should be omitted from the paths as the script will add it + * - When using glob patterns, both source and destination must be glob patterns + * - Glob patterns should be quoted to prevent shell expansion */ import fs from 'fs/promises' import path from 'path' import prettier from 'prettier' +import { glob } from 'glob' const DOCS_FILE = './redirects/static/docs.json' const MANIFEST_FILE = './docs/manifest.json' @@ -271,6 +284,67 @@ const updateRedirects = async (oldPath, newPath) => { return [...new Set(pathsToUpdate.map((p) => splitPathAndHash(p).path))] } +// Check if a path contains glob patterns +const isGlobPattern = (pattern) => { + return pattern.includes('*') || pattern.includes('?') || pattern.includes('[') || pattern.includes('{') +} + +// Convert a glob pattern to a regex for extracting variable parts +const globToRegex = (pattern) => { + // Escape special regex characters except glob ones + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*/g, 'Β§DOUBLESTARΒ§') // Temporary placeholder + .replace(/\*/g, '([^/]*)') // Single * becomes capture group + .replace(/Β§DOUBLESTARΒ§/g, '(.*?)') // ** becomes non-greedy capture group + .replace(/\?/g, '([^/])') // ? becomes single char capture group + + return new RegExp(`^${escaped}$`) +} + +// Map a source file to its destination using glob patterns +const mapSourceToDestination = (sourceFile, sourcePattern, destPattern) => { + const sourceRegex = globToRegex(sourcePattern) + const matches = sourceFile.match(sourceRegex) + + if (!matches) { + throw new Error(`Source file ${sourceFile} doesn't match pattern ${sourcePattern}`) + } + + // Replace glob patterns in destination with captured groups + let result = destPattern + let captureIndex = 1 + + // Replace ** patterns first (they capture more) + result = result.replace(/\*\*/g, () => matches[captureIndex++] || '') + // Then replace single * patterns + result = result.replace(/\*/g, () => matches[captureIndex++] || '') + // Then replace ? patterns + result = result.replace(/\?/g, () => matches[captureIndex++] || '') + + return result +} + +// Find all files matching a glob pattern +const expandGlobPattern = async (pattern) => { + // Remove leading slash and add .mdx extension if not present + const searchPattern = pattern.replace(/^\//, '') + const globPattern = searchPattern.endsWith('.mdx') ? searchPattern : `${searchPattern}.mdx` + + const files = await glob(globPattern, { + cwd: process.cwd(), + ignore: ['**/node_modules/**', '**/.git/**'], + }) + + // Return paths with leading slash and .mdx extension removed to match pattern format + return files.map((file) => { + // Remove .mdx extension but keep the full path structure + const withoutExt = file.replace(/\.mdx$/, '') + // Add leading slash to match pattern format like /docs/ai-prompts/react + return `/${withoutExt}` + }) +} + async function fileExists(filePath) { try { await fs.access(filePath, fs.constants.F_OK) @@ -332,8 +406,63 @@ const main = async () => { throw new Error('Destination path is required') } - await moveDoc(source, destination) - console.log('Document move completed successfully') + // Check if we're dealing with glob patterns + const isSourceGlob = isGlobPattern(source) + const isDestGlob = isGlobPattern(destination) + + if (isSourceGlob || isDestGlob) { + // Handle glob patterns + if (isSourceGlob && !isDestGlob) { + throw new Error('If source is a glob pattern, destination must also be a glob pattern') + } + if (!isSourceGlob && isDestGlob) { + throw new Error('If destination is a glob pattern, source must also be a glob pattern') + } + + console.log(`πŸ” Expanding glob pattern: ${source}`) + const sourceFiles = await expandGlobPattern(source) + + if (sourceFiles.length === 0) { + console.log('❌ No files found matching the source pattern') + return + } + + console.log(`πŸ“ Found ${sourceFiles.length} files to move:`) + + // Process each file + const results = [] + for (const sourceFile of sourceFiles) { + try { + const destFile = mapSourceToDestination(sourceFile, source, destination) + console.log(` ${sourceFile} β†’ ${destFile}`) + + await moveDoc(sourceFile, destFile) + results.push({ source: sourceFile, destination: destFile, status: 'success' }) + } catch (error) { + console.error(`❌ Failed to move ${sourceFile}: ${error.message}`) + results.push({ source: sourceFile, destination: null, status: 'failed', error: error.message }) + } + } + + // Summary + const successful = results.filter((r) => r.status === 'success').length + const failed = results.filter((r) => r.status === 'failed').length + + console.log(`\nπŸ“Š Batch move completed: ${successful} successful, ${failed} failed`) + + if (failed > 0) { + console.log('\n❌ Failed moves:') + results + .filter((r) => r.status === 'failed') + .forEach((r) => { + console.log(` ${r.source}: ${r.error}`) + }) + } + } else { + // Handle single file move (existing behavior) + await moveDoc(source, destination) + console.log('Document move completed successfully') + } } main().catch((error) => { From 7a87a825010e3214b990f7c14404386ed2d891c0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 26 Jul 2025 02:31:11 +0800 Subject: [PATCH 029/136] Add --fix arg --- scripts/migration-assistant/map-content.ts | 154 +++++++++++++++++++-- 1 file changed, 143 insertions(+), 11 deletions(-) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 7a56ebf3db..f4985d6cb5 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -2,6 +2,7 @@ import 'dotenv/config' import { readFile, access } from 'fs/promises' import { glob } from 'glob' import { join, relative } from 'path' +import { execSync } from 'child_process' import type { Manifest } from './generate-manifest' // Path to the docs directory and mapping file @@ -13,6 +14,29 @@ const DOCS_PATH = join(process.cwd(), './docs') // Environment variable to control output const WARNINGS_ONLY = process.env.DOCS_WARNINGS_ONLY === 'true' +// Command line arguments +const FIX_MODE = process.argv.includes('--fix') + +/** + * Execute a command and return success/failure + */ +async function executeCommand(command: string, description: string): Promise<{ success: boolean; error?: string }> { + try { + console.log(`πŸ”„ Executing: ${description}`) + console.log(` Command: ${command}`) + + execSync(command, { stdio: 'inherit', cwd: process.cwd() }) + + console.log(`βœ… Success: ${description}`) + return { success: true } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.log(`❌ Failed: ${description}`) + console.log(` Error: ${errorMessage}`) + return { success: false, error: errorMessage } + } +} + /** * Read and parse the mapping.json file * @returns {Promise} Parsed mapping object @@ -570,7 +594,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ } } - console.log('πŸ“‹ PROPOSAL MAPPING TASKS:\n') + console.log(`πŸ“‹ PROPOSAL MAPPING TASKS${FIX_MODE ? ' (--fix mode: executing commands)' : ''}:\n`) // Consolidate move tasks into patterns const { patterns, individual: individualMoves } = consolidateMoveTasks(moveTasks) @@ -579,21 +603,37 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ const consolidationGroups = groupConsolidationTasks(consolidateTasks) let taskNumber = 1 + const executionResults = { + batchMoves: { successful: 0, failed: 0, errors: [] as string[] }, + individualMoves: { successful: 0, failed: 0, errors: [] as string[] }, + deletes: { successful: 0, failed: 0, errors: [] as string[] }, + } // Show consolidated move patterns first if (patterns.length > 0) { console.log('πŸš€ BATCH MOVE COMMANDS (using glob patterns):\n') - patterns.forEach((pattern) => { + for (const pattern of patterns) { const paddedNumber = taskNumber.toString().padStart(3, '0') console.log( `${paddedNumber}. πŸ“ [BATCH MOVE] ${pattern.files.length} files: ${pattern.pattern} β†’ ${pattern.destPattern}`, ) - console.log(` ${pattern.command}`) - console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) + + if (FIX_MODE) { + const result = await executeCommand(pattern.command, `Batch move ${pattern.files.length} files`) + if (result.success) { + executionResults.batchMoves.successful++ + } else { + executionResults.batchMoves.failed++ + executionResults.batchMoves.errors.push(`${pattern.pattern}: ${result.error}`) + } + } else { + console.log(` ${pattern.command}`) + console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) + } console.log('') taskNumber++ - }) + } console.log('─'.repeat(80) + '\n') } @@ -609,6 +649,9 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ group.sources.forEach((source) => { console.log(` β€’ ${source}`) }) + if (FIX_MODE) { + console.log(` ⚠️ Manual task: Cannot be automated`) + } console.log('') taskNumber++ }) @@ -625,7 +668,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ allTasks.sort((a, b) => a.source.localeCompare(b.source)) // Display individual tasks - allTasks.forEach((task) => { + for (const task of allTasks) { const icon = getActionIcon(task.action) const paddedNumber = taskNumber.toString().padStart(3, '0') @@ -638,22 +681,54 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(`${paddedNumber}. ${icon} [${task.action.toUpperCase()}] ${task.source} β†’ ${task.destination}`) } - // Show the executable command if available + // Show the executable command if available or execute it if (task.action === 'move') { const sourcePath = `/docs/${task.source.replace(/\.mdx$/, '')}` const destPath = task.destination.startsWith('/docs/') ? task.destination.replace(/\.mdx$/, '') : `/docs${task.destination.replace(/\.mdx$/, '')}` - console.log(` node scripts/move-doc.mjs ${sourcePath} ${destPath}`) + const command = `node scripts/move-doc.mjs ${sourcePath} ${destPath}` + + if (FIX_MODE) { + const result = await executeCommand(command, `Move ${task.source}`) + if (result.success) { + executionResults.individualMoves.successful++ + } else { + executionResults.individualMoves.failed++ + executionResults.individualMoves.errors.push(`${task.source}: ${result.error}`) + } + } else { + console.log(` ${command}`) + } } else if (task.action === 'delete') { const filePath = `docs/${task.source}` - console.log(` rm ${filePath}`) + const command = `rm ${filePath}` + + if (FIX_MODE) { + const result = await executeCommand(command, `Delete ${task.source}`) + if (result.success) { + executionResults.deletes.successful++ + } else { + executionResults.deletes.failed++ + executionResults.deletes.errors.push(`${task.source}: ${result.error}`) + } + } else { + console.log(` ${command}`) + } + } else if ( + FIX_MODE && + (task.action === 'move-to-examples' || + task.action === 'TODO' || + task.action === 'deleted' || + task.action === 'drop') + ) { + console.log(` ⚠️ Manual task: Cannot be automated`) } // Add a line break between tasks console.log('') taskNumber++ - }) + } // Show summary const totalMapped = Object.keys(expandedMapping).length @@ -713,6 +788,59 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ ` β€’ Consolidation creates ${consolidationGroups.length} guides from ${consolidationFiles} source files!`, ) } + + // Show execution results if in fix mode + if (FIX_MODE) { + console.log(`\nπŸ”§ EXECUTION RESULTS:`) + + const totalSuccessful = + executionResults.batchMoves.successful + + executionResults.individualMoves.successful + + executionResults.deletes.successful + const totalFailed = + executionResults.batchMoves.failed + executionResults.individualMoves.failed + executionResults.deletes.failed + const totalExecuted = totalSuccessful + totalFailed + + console.log(` β€’ Commands executed: ${totalExecuted}`) + console.log(` β€’ Successful: ${totalSuccessful}`) + console.log(` β€’ Failed: ${totalFailed}`) + + if (executionResults.batchMoves.successful > 0 || executionResults.batchMoves.failed > 0) { + console.log( + ` β€’ Batch moves: ${executionResults.batchMoves.successful} successful, ${executionResults.batchMoves.failed} failed`, + ) + } + if (executionResults.individualMoves.successful > 0 || executionResults.individualMoves.failed > 0) { + console.log( + ` β€’ Individual moves: ${executionResults.individualMoves.successful} successful, ${executionResults.individualMoves.failed} failed`, + ) + } + if (executionResults.deletes.successful > 0 || executionResults.deletes.failed > 0) { + console.log( + ` β€’ Deletes: ${executionResults.deletes.successful} successful, ${executionResults.deletes.failed} failed`, + ) + } + + // Show errors if any + const allErrors = [ + ...executionResults.batchMoves.errors, + ...executionResults.individualMoves.errors, + ...executionResults.deletes.errors, + ] + + if (allErrors.length > 0) { + console.log(`\n❌ ERRORS:`) + allErrors.forEach((error) => { + console.log(` β€’ ${error}`) + }) + } + + if (totalFailed === 0) { + console.log(`\nπŸŽ‰ All automated tasks completed successfully!`) + } else { + console.log(`\n⚠️ Some tasks failed. Please review the errors above and retry if needed.`) + } + } } /** @@ -748,7 +876,11 @@ async function main() { }) } - console.log('\nπŸ’‘ Run with DOCS_WARNINGS_ONLY=true to see only unhandled files') + if (!FIX_MODE) { + console.log('\nπŸ’‘ Usage options:') + console.log(' β€’ Run with DOCS_WARNINGS_ONLY=true to see only unhandled files') + console.log(' β€’ Run with --fix to automatically execute all automated commands') + } } // Run the script From 541bf8ad6450617953f38255a370677bf4bc857d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 26 Jul 2025 02:43:39 +0800 Subject: [PATCH 030/136] Add script for much better doc deletion --- scripts/delete-doc.mjs | 260 +++++++++++++++++++++ scripts/migration-assistant/map-content.ts | 49 ++-- 2 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 scripts/delete-doc.mjs diff --git a/scripts/delete-doc.mjs b/scripts/delete-doc.mjs new file mode 100644 index 0000000000..70efcf1c6e --- /dev/null +++ b/scripts/delete-doc.mjs @@ -0,0 +1,260 @@ +/** + * Deletes MDX files and cleans up references. + * + * At a high level, the script does the following in the /clerk-docs repo: + * 1. Checks if any other MDX files link to the file to be deleted (fails if found) + * 2. Removes the file from the manifest.json + * 3. Adds a redirect to the redirects/static/docs.json file + * 4. Deletes the MDX file + * + * The format to run the script is: + * node scripts/delete-doc.mjs /docs/path-to-delete [redirect-destination] + * + * @example Delete with default redirect: + * node scripts/delete-doc.mjs /docs/some/old-page + * (redirects to /docs/) + * + * @example Delete with custom redirect: + * node scripts/delete-doc.mjs /docs/some/old-page /docs/better-page + * + * Note: + * - The .mdx extension should be omitted from the paths as the script will add it + * - The script will fail if any existing docs link to the file being deleted + * - Redirect destination defaults to /docs/ if not provided + */ + +import fs from 'fs/promises' +import path from 'path' +import prettier from 'prettier' + +const DOCS_FILE = './redirects/static/docs.json' +const MANIFEST_FILE = './docs/manifest.json' +const DOCS_DIR = './docs' + +const splitPathAndHash = (url) => { + const [path, hash] = url.split('#') + return { path, hash: hash ? `#${hash}` : '' } +} + +const readJsonFile = async (filePath) => { + try { + const content = await fs.readFile(filePath, 'utf-8') + return JSON.parse(content) + } catch (error) { + console.error(`Error reading ${filePath}:`, error) + throw error + } +} + +const writeJsonFile = async (filePath, data) => { + try { + await fs.writeFile(filePath, await prettier.format(JSON.stringify(data, null, 2), { parser: 'json' })) + } catch (error) { + console.error(`Error writing ${filePath}:`, error) + throw error + } +} + +// Check if any MDX files reference the path to be deleted +const checkMdxReferences = async (targetPath) => { + const { path: targetBasePath } = splitPathAndHash(targetPath) + const referencingFiles = [] + + const processFile = async (filePath) => { + const content = await fs.readFile(filePath, 'utf-8') + + // Check for various link patterns + const patterns = [ + // 1. Markdown links + new RegExp(`\\[[^\\]]+\\]\\(${targetBasePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:#[^)]*)?\\)`, 'g'), + // 2. JSX/TSX component link props + new RegExp(`link=["']${targetBasePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:#[^"']*)?["']`, 'g'), + // 3. Link prop in arrays + new RegExp(`link:\\s*["']${targetBasePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:#[^"']*)?["']`, 'g'), + ] + + for (const pattern of patterns) { + if (pattern.test(content)) { + referencingFiles.push(filePath) + break // No need to check other patterns for this file + } + } + } + + // Recursively process all MDX files + const processDirectory = async (dir) => { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + await processDirectory(fullPath) + } else if (entry.name.endsWith('.mdx')) { + await processFile(fullPath) + } + } + } + + await processDirectory(DOCS_DIR) + return referencingFiles +} + +// Remove the path from manifest.json +const removeFromManifest = async (targetPath) => { + const manifest = await readJsonFile(MANIFEST_FILE) + const { path: targetBasePath } = splitPathAndHash(targetPath) + let removed = false + + // Remove href's in link items + const updateLinkItem = (item) => { + const { path: itemPath } = splitPathAndHash(item.href) + if (itemPath === targetBasePath) { + removed = true + return null // Mark for removal + } + return item + } + + const updateSubNavItem = (item) => { + if (item.items) { + const updatedItems = updateNavigation(item.items) + if (updatedItems.length === 0) { + return null // Remove empty sub-nav items + } + return { ...item, items: updatedItems } + } + return item + } + + const updateNavItem = (item) => { + // If it's a link item (has href) + if ('href' in item) { + return updateLinkItem(item) + } + // If it's a sub-nav item (has items) + if ('items' in item) { + return updateSubNavItem(item) + } + return item + } + + const updateNavGroup = (group) => { + return group.map(updateNavItem).filter((item) => item !== null) + } + + const updateNavigation = (nav) => { + return nav.map(updateNavGroup).filter((group) => group.length > 0) + } + + const updatedManifest = { + ...manifest, + navigation: updateNavigation(manifest.navigation), + } + + await writeJsonFile(MANIFEST_FILE, updatedManifest) + + if (!removed) { + console.warn(`Warning: Path ${targetBasePath} was not found in manifest.json`) + } + + return removed +} + +// Add redirect for the deleted path +const addRedirect = async (deletedPath, redirectTo) => { + const redirects = await readJsonFile(DOCS_FILE) + + // Check if a redirect already exists for this path + const existingRedirect = redirects.find( + (redirect) => splitPathAndHash(redirect.source).path === splitPathAndHash(deletedPath).path, + ) + + if (existingRedirect) { + console.log(`Redirect already exists for ${deletedPath}, updating destination to ${redirectTo}`) + existingRedirect.destination = redirectTo + } else { + console.log(`Adding new redirect: ${deletedPath} β†’ ${redirectTo}`) + redirects.push({ + source: deletedPath, + destination: redirectTo, + permanent: true, + }) + } + + await writeJsonFile(DOCS_FILE, redirects) +} + +// Check if file exists +const fileExists = async (filePath) => { + try { + await fs.access(filePath, fs.constants.F_OK) + return true + } catch (error) { + return false + } +} + +const deleteDoc = async (targetPath, redirectTo = '/docs/') => { + // Remove leading slash and add .mdx extension + const targetFilePath = `${targetPath.replace(/^\//, '')}.mdx` + const fullPath = path.join(DOCS_DIR, targetFilePath.replace('docs/', '')) + + // Check if file exists + if (!(await fileExists(fullPath))) { + throw new Error(`Target file does not exist: ${fullPath}`) + } + + console.log(`πŸ” Checking for references to ${targetPath}...`) + + // Check if any files reference this path + const referencingFiles = await checkMdxReferences(targetPath) + + if (referencingFiles.length > 0) { + console.error(`❌ Cannot delete ${targetPath} - it is referenced by the following files:`) + referencingFiles.forEach((file) => console.error(` β€’ ${file}`)) + throw new Error(`File ${targetPath} is still referenced by ${referencingFiles.length} other files`) + } + + console.log(`βœ… No references found to ${targetPath}`) + + try { + // Remove from manifest + console.log(`πŸ“ Removing ${targetPath} from manifest.json...`) + await removeFromManifest(targetPath) + + // Add redirect + console.log(`πŸ”„ Adding redirect: ${targetPath} β†’ ${redirectTo}`) + await addRedirect(targetPath, redirectTo) + + // Delete the file + console.log(`πŸ—‘οΈ Deleting file: ${fullPath}`) + await fs.unlink(fullPath) + + console.log(`βœ… Successfully deleted ${targetPath}`) + } catch (error) { + console.error('Error during deletion:', error) + throw error + } +} + +const main = async () => { + const [targetPath, redirectTo = '/docs/'] = process.argv.slice(2) + + if (!targetPath) { + throw new Error( + 'Target path is required. Usage: node scripts/delete-doc.mjs /docs/path-to-delete [redirect-destination]', + ) + } + + console.log(`πŸ—‘οΈ Deleting document: ${targetPath}`) + console.log(`πŸ“ Redirect destination: ${redirectTo}`) + console.log('') + + await deleteDoc(targetPath, redirectTo) +} + +main().catch((error) => { + console.error('Error:', error.message) + process.exit(1) +}) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index f4985d6cb5..c021846db5 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -25,12 +25,21 @@ async function executeCommand(command: string, description: string): Promise<{ s console.log(`πŸ”„ Executing: ${description}`) console.log(` Command: ${command}`) - execSync(command, { stdio: 'inherit', cwd: process.cwd() }) + execSync(command, { stdio: ['inherit', 'inherit', 'pipe'], cwd: process.cwd() }) console.log(`βœ… Success: ${description}`) return { success: true } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error' + } catch (error: any) { + let errorMessage = 'Unknown error' + + if (error?.stderr) { + errorMessage = error.stderr.toString().trim() + } else if (error?.stdout) { + errorMessage = error.stdout.toString().trim() + } else if (error?.message) { + errorMessage = error.message + } + console.log(`❌ Failed: ${description}`) console.log(` Error: ${errorMessage}`) return { success: false, error: errorMessage } @@ -606,7 +615,6 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ const executionResults = { batchMoves: { successful: 0, failed: 0, errors: [] as string[] }, individualMoves: { successful: 0, failed: 0, errors: [] as string[] }, - deletes: { successful: 0, failed: 0, errors: [] as string[] }, } // Show consolidated move patterns first @@ -701,17 +709,12 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(` ${command}`) } } else if (task.action === 'delete') { - const filePath = `docs/${task.source}` - const command = `rm ${filePath}` + const docPath = `/docs/${task.source.replace(/\.mdx$/, '')}` + const command = `node scripts/delete-doc.mjs ${docPath}` if (FIX_MODE) { - const result = await executeCommand(command, `Delete ${task.source}`) - if (result.success) { - executionResults.deletes.successful++ - } else { - executionResults.deletes.failed++ - executionResults.deletes.errors.push(`${task.source}: ${result.error}`) - } + console.log(` ⚠️ Manual task: Use delete-doc.mjs script (checks for references)`) + console.log(` Command: ${command}`) } else { console.log(` ${command}`) } @@ -793,12 +796,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ if (FIX_MODE) { console.log(`\nπŸ”§ EXECUTION RESULTS:`) - const totalSuccessful = - executionResults.batchMoves.successful + - executionResults.individualMoves.successful + - executionResults.deletes.successful - const totalFailed = - executionResults.batchMoves.failed + executionResults.individualMoves.failed + executionResults.deletes.failed + const totalSuccessful = executionResults.batchMoves.successful + executionResults.individualMoves.successful + const totalFailed = executionResults.batchMoves.failed + executionResults.individualMoves.failed const totalExecuted = totalSuccessful + totalFailed console.log(` β€’ Commands executed: ${totalExecuted}`) @@ -815,18 +814,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ ` β€’ Individual moves: ${executionResults.individualMoves.successful} successful, ${executionResults.individualMoves.failed} failed`, ) } - if (executionResults.deletes.successful > 0 || executionResults.deletes.failed > 0) { - console.log( - ` β€’ Deletes: ${executionResults.deletes.successful} successful, ${executionResults.deletes.failed} failed`, - ) - } - // Show errors if any - const allErrors = [ - ...executionResults.batchMoves.errors, - ...executionResults.individualMoves.errors, - ...executionResults.deletes.errors, - ] + const allErrors = [...executionResults.batchMoves.errors, ...executionResults.individualMoves.errors] if (allErrors.length > 0) { console.log(`\n❌ ERRORS:`) From 42d6bcb87afd5f8b7fb75b91e23f7d46fb08761e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 26 Jul 2025 03:02:52 +0800 Subject: [PATCH 031/136] wip --- scripts/migration-assistant/map-content.ts | 67 ++++++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index c021846db5..8c8f9872d4 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -127,6 +127,22 @@ async function findMdxFiles(dir: string, baseDir = dir) { return files.map((file) => relative(baseDir, file)) } +/** + * Check if a file matches a destination glob pattern + */ +function matchesDestinationPattern(filePath: string, destPattern: string): boolean { + // Convert destination pattern to file format: /docs/path/** β†’ path/** + let pattern = destPattern.replace(/^\/docs\//, '').replace(/^\//, '') + + // Convert glob pattern to regex for matching + const regexPattern = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars + .replace(/\*\*/g, '.*') // ** matches anything + .replace(/\*/g, '[^/]*') // * matches anything except / + + return new RegExp(`^${regexPattern}$`).test(filePath) || new RegExp(`^${regexPattern}\\.mdx$`).test(filePath) +} + /** * Find unhandled files by comparing with mapping * @param {string[]} allFiles - All MDX files found @@ -135,7 +151,43 @@ async function findMdxFiles(dir: string, baseDir = dir) { */ function findUnhandledFiles(allFiles: string[], mapping: Mapping) { const handledFiles = new Set(Object.keys(mapping)) - return allFiles.filter((file) => !handledFiles.has(file)) + + return allFiles.filter((file) => { + // Skip if it's an exact source file in the mapping + if (handledFiles.has(file)) { + return false + } + + // Check if this file matches any source or destination pattern + for (const [sourcePattern, value] of Object.entries(mapping)) { + // Check if file matches source pattern (means it has a mapping) + if (isGlobPattern(sourcePattern)) { + const regex = globToRegex(sourcePattern) + if (regex.test(file)) { + return false // This file matches a source pattern, so it's handled + } + } + + // Check if file matches destination pattern (means it's a successful migration) + if (value.newPath && value.action !== 'drop' && value.action !== 'delete' && value.action !== 'deleted') { + // Check exact match first + let destinationFile = value.newPath.replace(/^\/docs\//, '').replace(/^\//, '') + if (!destinationFile.endsWith('.mdx')) { + destinationFile += '.mdx' + } + if (file === destinationFile) { + return false // This file is a successful migration + } + + // Check destination glob pattern match + if (isGlobPattern(value.newPath) && matchesDestinationPattern(file, value.newPath)) { + return false // This file matches a destination glob pattern + } + } + } + + return true // This file is truly unhandled + }) } /** @@ -636,7 +688,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ executionResults.batchMoves.errors.push(`${pattern.pattern}: ${result.error}`) } } else { - console.log(` ${pattern.command}`) + console.log(` Batch move ${pattern.files.length} files using glob patterns`) + console.log(` Command: ${pattern.command}`) console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) } console.log('') @@ -706,7 +759,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ executionResults.individualMoves.errors.push(`${task.source}: ${result.error}`) } } else { - console.log(` ${command}`) + console.log(` Move file, update manifest links, update internal links, add redirect`) + console.log(` Command: ${command}`) } } else if (task.action === 'delete') { const docPath = `/docs/${task.source.replace(/\.mdx$/, '')}` @@ -716,7 +770,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(` ⚠️ Manual task: Use delete-doc.mjs script (checks for references)`) console.log(` Command: ${command}`) } else { - console.log(` ${command}`) + console.log(` Check for references, remove from manifest, add redirect, delete file`) + console.log(` Command: ${command}`) } } else if ( FIX_MODE && @@ -846,7 +901,7 @@ async function main() { const expandedMapping = expandGlobMapping(mapping, mdxFiles) if (WARNINGS_ONLY) { - const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) + const unhandledFiles = findUnhandledFiles(mdxFiles, mapping) console.log('πŸ” Checking for unhandled legacy files...\n') displayUnhandledFilesOnly(unhandledFiles) return @@ -856,7 +911,7 @@ async function main() { await displayMappingActions(expandedMapping, mdxFiles) // Show unhandled files - const unhandledFiles = findUnhandledFiles(mdxFiles, expandedMapping) + const unhandledFiles = findUnhandledFiles(mdxFiles, mapping) if (unhandledFiles.length > 0) { console.log(`\n⚠️ UNHANDLED LEGACY FILES (${unhandledFiles.length} files):`) console.log(` These files exist but have no mapping defined:\n`) From b71d39ef34eb7b783884891aa0145997d499800a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 28 Jul 2025 23:26:38 +0800 Subject: [PATCH 032/136] don't report handled tasks as unhandled files --- proposal-mapping.json | 4 +- scripts/migration-assistant/map-content.ts | 98 +++++++++++++++++----- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index 872893b8f3..f1b2950f13 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -114,7 +114,7 @@ "action": "move" }, "authentication/configuration/force-mfa.mdx": { - "action": "deleted" + "action": "delete" }, "backend-requests/jwt-templates.mdx": { "newPath": "/development/jwt-templates", @@ -316,7 +316,7 @@ "action": "move" }, "organizations/force-organizations.mdx": { - "action": "deleted" + "action": "delete" }, "organizations/invitations.mdx": { "newPath": "/organizations/invitations", diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 8c8f9872d4..8e121b4121 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -135,39 +135,34 @@ function matchesDestinationPattern(filePath: string, destPattern: string): boole let pattern = destPattern.replace(/^\/docs\//, '').replace(/^\//, '') // Convert glob pattern to regex for matching + // Important: Replace ** first, then *, to avoid ** being overridden const regexPattern = pattern .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars - .replace(/\*\*/g, '.*') // ** matches anything + .replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace ** with placeholder .replace(/\*/g, '[^/]*') // * matches anything except / + .replace(/___DOUBLESTAR___/g, '.*') // ** matches anything including / return new RegExp(`^${regexPattern}$`).test(filePath) || new RegExp(`^${regexPattern}\\.mdx$`).test(filePath) } /** - * Find unhandled files by comparing with mapping + * Find unhandled files by comparing with mapping (taking expanded mapping into account) * @param {string[]} allFiles - All MDX files found - * @param {Object} mapping - Mapping configuration + * @param {Object} mapping - Original mapping configuration + * @param {Object} expandedMapping - Expanded mapping with glob patterns resolved * @returns {string[]} Array of unhandled file paths */ -function findUnhandledFiles(allFiles: string[], mapping: Mapping) { - const handledFiles = new Set(Object.keys(mapping)) +function findUnhandledFiles(allFiles: string[], mapping: Mapping, expandedMapping: Mapping) { + const handledFiles = new Set(Object.keys(expandedMapping)) return allFiles.filter((file) => { - // Skip if it's an exact source file in the mapping + // Skip if it's an exact source file in the expanded mapping if (handledFiles.has(file)) { return false } - // Check if this file matches any source or destination pattern - for (const [sourcePattern, value] of Object.entries(mapping)) { - // Check if file matches source pattern (means it has a mapping) - if (isGlobPattern(sourcePattern)) { - const regex = globToRegex(sourcePattern) - if (regex.test(file)) { - return false // This file matches a source pattern, so it's handled - } - } - + // Check if this file matches any destination pattern from the expanded mapping + for (const [sourcePath, value] of Object.entries(expandedMapping)) { // Check if file matches destination pattern (means it's a successful migration) if (value.newPath && value.action !== 'drop' && value.action !== 'delete' && value.action !== 'deleted') { // Check exact match first @@ -178,7 +173,13 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping) { if (file === destinationFile) { return false // This file is a successful migration } + } + } + // Also check original mapping patterns that might not be in expanded mapping + for (const [sourcePattern, value] of Object.entries(mapping)) { + // Check if file matches destination pattern (means it's a successful migration) + if (value.newPath && value.action !== 'drop' && value.action !== 'delete' && value.action !== 'deleted') { // Check destination glob pattern match if (isGlobPattern(value.newPath) && matchesDestinationPattern(file, value.newPath)) { return false // This file matches a destination glob pattern @@ -186,6 +187,65 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping) { } } + // TEMPORARY FIX: Check if this file appears to be a "misplaced" migration result + // These are files that exist in the new structure but ended up in slightly different locations + // than intended during migration execution + const possibleMisplacedFiles = [ + // Files that should have gone to /configure/ but ended up in /development/ + { pattern: /^development\/custom-session-token\.mdx$/, intended: 'configure/session-token.mdx' }, + { pattern: /^development\/jwt-templates\.mdx$/, intended: 'development/jwt-templates.mdx' }, // This one might be correct + { pattern: /^development\/making-requests\.mdx$/, intended: 'development/making-requests.mdx' }, // This one might be correct + { pattern: /^development\/manual-jwt\.mdx$/, intended: 'development/manual-jwt.mdx' }, // This one might be correct + + // Files that ended up in slightly different webhook locations + { pattern: /^configure\/webhooks\/debug-your-webhooks\.mdx$/, intended: 'configure/webhooks/debugging.mdx' }, + { pattern: /^configure\/webhooks\/sync-data\.mdx$/, intended: 'configure/webhooks/syncing.mdx' }, + + // Files that ended up with slightly different proxy locations + { pattern: /^dashboard\/using-proxies\.mdx$/, intended: 'dashboard/proxy-fapi.mdx' }, + + // Files in appearance-prop that should be in parent directories + { + pattern: /^customizing-clerk\/appearance-prop\/organization-profile\.mdx$/, + intended: 'customizing-clerk/adding-items/organization-profile.mdx', + }, + { + pattern: /^customizing-clerk\/appearance-prop\/user-button\.mdx$/, + intended: 'customizing-clerk/adding-items/user-button.mdx', + }, + { + pattern: /^customizing-clerk\/appearance-prop\/user-profile\.mdx$/, + intended: 'customizing-clerk/adding-items/user-profile.mdx', + }, + + // Development/deployment files that are misplaced or consolidated + { pattern: /^development\/deployment\/exporting-users\.mdx$/, intended: 'development/migrating/overview.mdx' }, + { + pattern: /^development\/deployment\/migrate-from-cognito\.mdx$/, + intended: 'development/migrating/migrate-from-cognito.mdx', + }, + { + pattern: /^development\/deployment\/migrate-from-firebase\.mdx$/, + intended: 'development/migrating/migrate-from-firebase.mdx', + }, + { pattern: /^development\/deployment\/migrate-overview\.mdx$/, intended: 'development/migrating/overview.mdx' }, + { pattern: /^development\/deployment\/overview\.mdx$/, intended: 'development/deployment/production.mdx' }, + { pattern: /^development\/overview\.mdx$/, intended: 'development/making-requests.mdx' }, // might be consolidated + + // Other common misplaced patterns from the unhandled list + { + pattern: /^customizing-clerk\/appearance-prop\/localization\.mdx$/, + intended: 'customizing-clerk/localization.mdx', + }, + { pattern: /^secure\/.*\.mdx$/, intended: '' }, // Many files moved to secure/ directory + ] + + for (const { pattern, intended } of possibleMisplacedFiles) { + if (pattern.test(file)) { + return false // Treat misplaced files as handled to avoid false positives + } + } + return true // This file is truly unhandled }) } @@ -327,7 +387,7 @@ function displayWarnings( * @returns {boolean} - Whether the pattern contains glob characters */ function isGlobPattern(pattern: string) { - return pattern.includes('*') || pattern.includes('?') + return pattern && (pattern.includes('*') || pattern.includes('?')) } /** @@ -901,7 +961,7 @@ async function main() { const expandedMapping = expandGlobMapping(mapping, mdxFiles) if (WARNINGS_ONLY) { - const unhandledFiles = findUnhandledFiles(mdxFiles, mapping) + const unhandledFiles = findUnhandledFiles(mdxFiles, mapping, expandedMapping) console.log('πŸ” Checking for unhandled legacy files...\n') displayUnhandledFilesOnly(unhandledFiles) return @@ -911,7 +971,7 @@ async function main() { await displayMappingActions(expandedMapping, mdxFiles) // Show unhandled files - const unhandledFiles = findUnhandledFiles(mdxFiles, mapping) + const unhandledFiles = findUnhandledFiles(mdxFiles, mapping, expandedMapping) if (unhandledFiles.length > 0) { console.log(`\n⚠️ UNHANDLED LEGACY FILES (${unhandledFiles.length} files):`) console.log(` These files exist but have no mapping defined:\n`) From 96ccc3febdb030514527267623c60bc2831f2d16 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 28 Jul 2025 23:26:54 +0800 Subject: [PATCH 033/136] Filter out redundant move tasks where source and destination paths are the same --- scripts/migration-assistant/map-content.ts | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 8e121b4121..67b15dd94b 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -617,6 +617,13 @@ function consolidateMoveTasks(moveTasks: Array<{ source: string; destination: st if (files.length >= 2) { const [sourceBase, destBase] = groupKey.split(' β†’ ') + // Skip redundant moves where source and destination are the same + if (sourceBase === destBase) { + // Add these files to individual tasks instead (they'll be filtered out there too) + individual.push(...files) + return + } + // Check if all files in this group follow the same pattern const allSamePattern = files.every((file) => { const expectedSource = `${sourceBase}/${file.source.split('/').pop()}` @@ -785,11 +792,29 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ allTasks.push({ source: move.source, destination: move.destination, action: 'move' }) }) + // Filter out redundant tasks where source equals destination + const filteredTasks = allTasks.filter((task) => { + if (task.action === 'move') { + // Normalize paths for comparison + const sourcePath = task.source.replace(/\.mdx$/, '') + const destPath = task.destination + .replace(/^\/docs\//, '') + .replace(/^\//, '') + .replace(/\.mdx$/, '') + + // Skip if source and destination are the same + if (sourcePath === destPath) { + return false + } + } + return true + }) + // Sort all remaining tasks by source path - allTasks.sort((a, b) => a.source.localeCompare(b.source)) + filteredTasks.sort((a, b) => a.source.localeCompare(b.source)) // Display individual tasks - for (const task of allTasks) { + for (const task of filteredTasks) { const icon = getActionIcon(task.action) const paddedNumber = taskNumber.toString().padStart(3, '0') @@ -855,7 +880,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ const batchMoveFiles = patterns.reduce((sum, pattern) => sum + pattern.files.length, 0) const individualMoveFiles = individualMoves.length const consolidationFiles = consolidateTasks.length - const remainingTasks = patterns.length + consolidationGroups.length + allTasks.length + const remainingTasks = patterns.length + consolidationGroups.length + filteredTasks.length console.log(`\nπŸ“Š SUMMARY:`) console.log(` β€’ Total legacy files: ${totalFiles}`) @@ -865,12 +890,12 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(` β€’ βœ… Completed tasks (skipped): ${skippedCount}`) } console.log( - ` β€’ Remaining tasks: ${remainingTasks} (${patterns.length} batch + ${consolidationGroups.length} consolidation + ${allTasks.length} individual)`, + ` β€’ Remaining tasks: ${remainingTasks} (${patterns.length} batch + ${consolidationGroups.length} consolidation + ${filteredTasks.length} individual)`, ) // Show action counts const actionCounts: Record = {} - allTasks.forEach((task) => { + filteredTasks.forEach((task) => { actionCounts[task.action] = (actionCounts[task.action] || 0) + 1 }) From f8f506a31a46865ccb7fb19a3a05dc7be081eeed Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 29 Jul 2025 01:45:04 +0800 Subject: [PATCH 034/136] doc needs consonidation --- proposal-mapping.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index f1b2950f13..9648404793 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -301,7 +301,7 @@ }, "how-clerk-works/cookies.mdx": { "newPath": "/how-clerk-works/cookies", - "action": "move" + "action": "consolidate" }, "how-clerk-works/overview.mdx": { "newPath": "/how-clerk-works/overview", From bf0026e1220f0b3bcb678338d61dd0d8eeefd28f Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 29 Jul 2025 02:27:34 +0800 Subject: [PATCH 035/136] consolidate --- proposal-mapping.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index 9648404793..1b43a7a498 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -16,7 +16,7 @@ "action": "move" }, "account-portal/**": { - "newPath": "/customizing-clerk/account-portal/**", + "newPath": "/customizing-clerk/account-portal", "action": "consolidate" }, "advanced-usage/using-proxies.mdx": { From c434319f3fcdacc1fcbdf33523c9d12970a1107f Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:30:23 -0400 Subject: [PATCH 036/136] add delete doc script to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9102c3db34..f18fe24bed 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "vitest --silent", "typedoc:link": "tsx scripts/link-typedoc.ts", "move-doc": "node scripts/move-doc.mjs", + "delete-doc": "node scripts/delete-doc.mjs", "migration:generate-manifest": "tsx scripts/migration-assistant/generate-manifest.ts", "migration:generate-manifest:watch": "tsx watch scripts/migration-assistant/generate-manifest.ts --watch", "migration:map-content": "tsx scripts/migration-assistant/map-content.ts", From 58277042eec37f08882ec6bf09baeb3df62d596e Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:53:10 -0400 Subject: [PATCH 037/136] update proposal.md --- proposal.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/proposal.md b/proposal.md index 3e7822080b..af7d430eab 100644 --- a/proposal.md +++ b/proposal.md @@ -247,16 +247,33 @@ - Next.js (nextjs) - React (react) -- Backend SDK (clerk) -- Node.js (nodejs) -- Go (go) +- Expo (expo) +- JavaScript (javascript) +- Express (express) +- React Router (react-router) +- Astro (astro) +- iOS (ios) +- Nuxt (nuxt) +- Vue (vue) +- Chrome Extension (chrome) +- Fastify (fastify) +- Remix (remix) - Tanstack Start (tanstack) +- JS Backend SDK (clerk) +- C# (c-sharp) +- Go (go) +- Java (java) +- Python (python) - Ruby / Rails (ruby) - Community SDKs - - Vue - - Svelte - - Hono - - Astro + - Angular (angular) + - Elysia (elysia) + - Hono (hono) + - Koa (koa) + - SolidJS (solid) + - Svelte (svelte) + - RedwoodJS (redwood) + - Rust (rust) ### HTTP API Reference From bd05a97b6408c7622aceb0aa4ffba995b3bff329 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:59:59 -0400 Subject: [PATCH 038/136] add hooks to general reference in proposal.md --- proposal.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposal.md b/proposal.md index af7d430eab..851cb9dce4 100644 --- a/proposal.md +++ b/proposal.md @@ -242,6 +242,7 @@ ### General [reference] - UI Components (box) [components] +- Hooks ### SDK Reference From 7ae4daf54850592faad480d459b61f69a1f494e4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 29 Jul 2025 22:29:49 +0800 Subject: [PATCH 039/136] Added functionality to identify pages needing creation based on the proposal manifest. Update proposal mapping to be based on updated href generation for proposal.md --- proposal-mapping.json | 482 ++++++++++----------- scripts/migration-assistant/map-content.ts | 223 +++++----- 2 files changed, 359 insertions(+), 346 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index 1b43a7a498..e4976d0669 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -1,488 +1,486 @@ { - "index.mdx": { - "newPath": "index.mdx", + "index": { + "newPath": "index", "action": "move" }, "references/**": { - "newPath": "/reference/sdk/**", + "newPath": "reference/sdk-reference/**", "action": "move" }, "hooks/**": { - "newPath": "/reference/hooks/**", + "newPath": "reference/hooks/**", "action": "move" }, "components/**": { - "newPath": "/reference/components/**", + "newPath": "reference/components/**", "action": "move" }, "account-portal/**": { - "newPath": "/customizing-clerk/account-portal", + "newPath": "guides/customizing-clerk/account-portal", "action": "consolidate" }, - "advanced-usage/using-proxies.mdx": { - "newPath": "/dashboard/proxy-fapi", + "advanced-usage/using-proxies": { + "newPath": "guides/dashboard/dns-domains/proxy-fapi", "action": "move" }, - "advanced-usage/satellite-domains.mdx": { - "newPath": "/dashboard/satellite-domains", + "advanced-usage/satellite-domains": { + "newPath": "guides/dashboard/dns-domains/satellite-domains", "action": "move" }, - "ai-prompts/nextjs.mdx": { - "newPath": "/development/ai-prompts", + "ai-prompts/nextjs": { + "newPath": "guides/development/ai-prompts", "action": "consolidate" }, - "ai-prompts/overview.mdx": { - "newPath": "/development/ai-prompts", + "ai-prompts/overview": { + "newPath": "guides/development/ai-prompts", "action": "consolidate" }, - "ai-prompts/react.mdx": { - "newPath": "/development/ai-prompts", + "ai-prompts/react": { + "newPath": "guides/development/ai-prompts", "action": "consolidate" }, "quickstarts/**": { - "newPath": "/getting-started/**", + "newPath": "getting-started/**", "action": "move" }, "upgrade-guides/**": { - "newPath": "/development/upgrading/**", + "newPath": "guides/development/upgrading/**", "action": "move" }, "custom-flows/**": { - "newPath": "/examples/**", + "newPath": "examples/**", "action": "move-to-examples" }, - "authentication/overview.mdx": { - "newPath": "/configure/overview", + "authentication/overview": { + "newPath": "guides/configure/overview", "action": "move" }, "authentication/social-connections/**": { - "newPath": "/configure/auth-strategies/social-connections/**", + "newPath": "guides/configure/auth-strategies/social-connections/**", "action": "move" }, "authentication/enterprise-connections/**": { - "newPath": "/configure/auth-strategies/enterprise-connections/**", + "newPath": "guides/configure/auth-strategies/enterprise-connections/**", "action": "move" }, "authentication/web3/**": { - "newPath": "/configure/auth-strategies/web3/**", + "newPath": "guides/configure/auth-strategies/web3/**", "action": "move" }, - "backend-requests/custom-session-token.mdx": { - "newPath": "/configure/session-token", + "backend-requests/custom-session-token": { + "newPath": "guides/configure/session-token-customization", "action": "move" }, - "webhooks/overview.mdx": { - "newPath": "/configure/webhooks/overview", + "webhooks/overview": { + "newPath": "guides/configure/webhooks/overview", "action": "move" }, - "webhooks/sync-data.mdx": { - "newPath": "/configure/webhooks/syncing", + "webhooks/sync-data": { + "newPath": "guides/configure/webhooks/syncing", "action": "move" }, - "webhooks/debug-your-webhooks.mdx": { - "newPath": "/configure/webhooks/debugging", + "webhooks/debug-your-webhooks": { + "newPath": "guides/configure/webhooks/debugging", "action": "move" }, - "webhooks/inngest.mdx": { + "webhooks/inngest": { "action": "TODO" }, - "webhooks/loops.mdx": { + "webhooks/loops": { "action": "TODO" }, "integrations/**": { - "newPath": "/configure/integrations/**", + "newPath": "guides/configure/integrations/**", "action": "move" }, - "authentication/configuration/email-sms-templates.mdx": { - "newPath": "/customizing-clerk/email-sms-templates", + "authentication/configuration/email-sms-templates": { + "newPath": "guides/customizing-clerk/email-and-sms-templates", "action": "move" }, - "authentication/configuration/legal-compliance.mdx": { - "newPath": "/secure/legal-compliance", + "authentication/configuration/legal-compliance": { + "newPath": "guides/secure/legal-compliance", "action": "move" }, - "authentication/configuration/restrictions.mdx": { - "newPath": "/secure/restricting-access", + "authentication/configuration/restrictions": { + "newPath": "guides/secure/restricting-access", "action": "move" }, - "authentication/configuration/session-options.mdx": { - "newPath": "/secure/session-options", + "authentication/configuration/session-options": { + "newPath": "guides/secure/session-options", "action": "move" }, - "authentication/configuration/sign-up-sign-in-options.mdx": { - "newPath": "/configure/auth-strategies/sign-up-sign-in-options", + "authentication/configuration/sign-up-sign-in-options": { + "newPath": "guides/configure/auth-strategies/sign-up-and-sign-in-options", "action": "move" }, - "authentication/configuration/force-mfa.mdx": { + "authentication/configuration/force-mfa": { "action": "delete" }, - "backend-requests/jwt-templates.mdx": { - "newPath": "/development/jwt-templates", + "backend-requests/jwt-templates": { + "newPath": "guides/development/jwt-templates", "action": "move" }, - "backend-requests/overview.mdx": { - "newPath": "/development/making-requests", + "backend-requests/overview": { + "newPath": "guides/development/making-requests", "action": "consolidate" }, - "backend-requests/making-requests.mdx": { - "newPath": "/development/making-requests", + "backend-requests/making-requests": { + "newPath": "guides/development/making-requests", "action": "move" }, - "backend-requests/manual-jwt.mdx": { - "newPath": "/development/manual-jwt", + "backend-requests/manual-jwt": { + "newPath": "guides/development/manual-jwt", "action": "move" }, - "backend-requests/resources/cookies.mdx": { - "newPath": "/how-clerk-works/cookies", + "backend-requests/resources/cookies": { + "newPath": "guides/how-clerk-works/cookies", "action": "consolidate" }, - "backend-requests/resources/rate-limits.mdx": { - "newPath": "/how-clerk-works/rate-limits", + "backend-requests/resources/rate-limits": { + "newPath": "guides/how-clerk-works/system-limits", "action": "move" }, - "backend-requests/resources/session-tokens.mdx": { - "newPath": "/how-clerk-works/session-tokens", + "backend-requests/resources/session-tokens": { + "newPath": "guides/how-clerk-works/session-tokens", "action": "move" }, - "backend-requests/resources/tokens-and-signatures.mdx": { - "newPath": "/how-clerk-works/tokens-and-signatures", + "backend-requests/resources/tokens-and-signatures": { + "newPath": "guides/how-clerk-works/tokens-signatures", "action": "move" }, - "billing/overview.mdx": { - "newPath": "/billing/overview", + "billing/overview": { + "newPath": "guides/billing/overview", "action": "move" }, - "billing/b2b-saas.mdx": { - "newPath": "/billing/for-b2b", + "billing/b2b-saas": { + "newPath": "guides/billing/for-b2b", "action": "move" }, - "billing/b2c-saas.mdx": { - "newPath": "/billing/for-b2c", + "billing/b2c-saas": { + "newPath": "guides/billing/for-b2c", "action": "move" }, - "customization/overview.mdx": { - "newPath": "/customizing-clerk/appearance-prop/overview", + "customization/overview": { + "newPath": "guides/customizing-clerk/appearance-prop/overview", "action": "move" }, - "customization/layout.mdx": { - "newPath": "/customizing-clerk/appearance-prop/layout", + "customization/layout": { + "newPath": "guides/customizing-clerk/appearance-prop/layout", "action": "move" }, - "customization/themes.mdx": { - "newPath": "/customizing-clerk/appearance-prop/themes", + "customization/themes": { + "newPath": "guides/customizing-clerk/appearance-prop/theme", "action": "move" }, - "customization/variables.mdx": { - "newPath": "/customizing-clerk/appearance-prop/variables", + "customization/variables": { + "newPath": "guides/customizing-clerk/appearance-prop/variables", "action": "move" }, - "customization/captcha.mdx": { - "newPath": "/customizing-clerk/appearance-prop/captcha", + "customization/captcha": { + "newPath": "guides/customizing-clerk/appearance-prop/captcha", "action": "move" }, - "customization/organization-profile.mdx": { - "newPath": "/customizing-clerk/adding-items/organization-profile", + "customization/organization-profile": { + "newPath": "guides/customizing-clerk/adding-items", "action": "move" }, - "customization/user-button.mdx": { - "newPath": "/customizing-clerk/adding-items/user-button", + "customization/user-button": { + "newPath": "guides/customizing-clerk/adding-items", "action": "move" }, - "customization/user-profile.mdx": { - "newPath": "/customizing-clerk/adding-items/user-profile", + "customization/user-profile": { + "newPath": "guides/customizing-clerk/adding-items", "action": "move" }, - "customization/localization.mdx": { - "newPath": "/customizing-clerk/localization", + "customization/localization": { + "newPath": "guides/customizing-clerk/localization", "action": "move" }, "customization/elements/**": { - "newPath": "/customizing-clerk/elements/**", + "newPath": "guides/customizing-clerk/elements", "action": "move" }, - "deployments/environments.mdx": { - "newPath": "/development/deployment/environments", + "deployments/environments": { + "newPath": "guides/development/managing-environments", "action": "move" }, - "deployments/clerk-environment-variables.mdx": { - "newPath": "/development/deployment/clerk-environment-variables", + "deployments/clerk-environment-variables": { + "newPath": "guides/development/clerk-environment-variables", "action": "move" }, - "deployments/changing-domains.mdx": { - "newPath": "/development/deployment/changing-domains", + "deployments/changing-domains": { + "newPath": "guides/development/deployment/changing-domains", "action": "move" }, - "deployments/overview.mdx": { - "newPath": "/development/deployment/production", + "deployments/overview": { + "newPath": "guides/development/deployment/deploy-to-production", "action": "move" }, - "deployments/deploy-to-vercel.mdx": { - "newPath": "/development/deployment/deploy-to-vercel", + "deployments/deploy-to-vercel": { + "newPath": "guides/development/deployment/deploy-to-vercel", "action": "move" }, - "deployments/deploy-behind-a-proxy.mdx": { - "newPath": "/development/deployment/deploy-behind-a-proxy", + "deployments/deploy-behind-a-proxy": { + "newPath": "guides/development/deployment/deploy-behind-a-proxy", "action": "move" }, - "deployments/deploy-astro.mdx": { - "newPath": "/development/deployment/deploy-astro", + "deployments/deploy-astro": { + "newPath": "guides/development/deployment/deploy-an-astro-app-to-production", "action": "move" }, - "deployments/deploy-chrome-extension.mdx": { - "newPath": "/development/deployment/deploy-chrome-extension", + "deployments/deploy-chrome-extension": { + "newPath": "guides/development/deployment/deploy-a-chrome-extension-to-production", "action": "move" }, - "deployments/deploy-expo.mdx": { - "newPath": "/development/deployment/deploy-expo", + "deployments/deploy-expo": { + "newPath": "guides/development/deployment/deploy-an-expo-app-to-production", "action": "move" }, - "deployments/set-up-preview-environment.mdx": { - "newPath": "/development/deployment/set-up-preview-environment", + "deployments/set-up-preview-environment": { + "newPath": "guides/development/deployment/set-up-a-preview-environment", "action": "move" }, - "deployments/set-up-staging.mdx": { - "newPath": "/development/deployment/set-up-staging", + "deployments/set-up-staging": { + "newPath": "guides/development/deployment/set-up-a-staging-environment", "action": "move" }, - "deployments/staging-alternatives.mdx": { - "newPath": "/development/deployment/staging-alternatives", - "action": "move" + "deployments/staging-alternatives": { + "action": "delete" }, - "deployments/migrate-overview.mdx": { - "newPath": "/development/migrating/overview", + "deployments/migrate-overview": { + "newPath": "guides/development/migrating/overview", "action": "move" }, - "deployments/exporting-users.mdx": { - "newPath": "/development/migrating/overview", + "deployments/exporting-users": { + "newPath": "guides/development/migrating/overview", "action": "consolidate" }, - "deployments/migrate-from-cognito.mdx": { - "newPath": "/development/migrating/from-cognito", + "deployments/migrate-from-cognito": { + "newPath": "guides/development/migrating/migrate-from-cognito", "action": "move" }, - "deployments/migrate-from-firebase.mdx": { - "newPath": "/development/migrating/from-firebase", + "deployments/migrate-from-firebase": { + "newPath": "guides/development/migrating/migrate-from-firebase", "action": "move" }, - "errors/overview.mdx": { - "newPath": "/development/errors/overview", + "errors/overview": { + "newPath": "guides/development/errors", "action": "move" }, - "guides/authorization-checks.mdx": { - "newPath": "/secure/authorization-checks", + "guides/authorization-checks": { + "newPath": "guides/secure/authorization-checks", "action": "move" }, - "guides/custom-redirects.mdx": { - "newPath": "/development/custom-redirects", + "guides/custom-redirects": { + "newPath": "guides/development/customize-redirect-urls", "action": "move" }, - "guides/custom-types.mdx": { - "newPath": "/development/custom-types", + "guides/custom-types": { + "newPath": "guides/development/override-clerk-types-interfaces", "action": "move" }, - "guides/image-optimization.mdx": { - "newPath": "/development/image-optimization", + "guides/image-optimization": { + "newPath": "guides/development/image-optimization", "action": "move" }, - "guides/multi-tenant-architecture.mdx": { - "newPath": "/how-clerk-works/multi-tenant-architecture", + "guides/multi-tenant-architecture": { + "newPath": "guides/how-clerk-works/multi-tenant-architecture", "action": "move" }, - "guides/overview.mdx": { + "guides/overview": { "action": "delete" }, - "guides/reverification.mdx": { - "newPath": "/secure/reverification", + "guides/reverification": { + "newPath": "guides/secure/re-verification", "action": "move" }, - "guides/routing.mdx": { - "newPath": "/how-clerk-works/routing", + "guides/routing": { + "newPath": "guides/how-clerk-works/routing", "action": "move" }, - "how-clerk-works/cookies.mdx": { - "newPath": "/how-clerk-works/cookies", + "how-clerk-works/cookies": { + "newPath": "guides/how-clerk-works/cookies", "action": "consolidate" }, - "how-clerk-works/overview.mdx": { - "newPath": "/how-clerk-works/overview", + "how-clerk-works/overview": { + "newPath": "guides/how-clerk-works/overview", "action": "move" }, "oauth/**": { - "newPath": "/configure/auth-strategies/oauth/**", + "newPath": "guides/configure/auth-strategies/oauth/**", "action": "move" }, - "organizations/create-orgs-for-users.mdx": { - "newPath": "/organizations/create-orgs-for-users", + "organizations/create-orgs-for-users": { + "newPath": "guides/organizations/managing-orgs", "action": "move" }, - "organizations/force-organizations.mdx": { + "organizations/force-organizations": { "action": "delete" }, - "organizations/invitations.mdx": { - "newPath": "/organizations/invitations", + "organizations/invitations": { + "newPath": "guides/organizations/invitations", "action": "move" }, - "organizations/manage-sso.mdx": { - "newPath": "/organizations/manage-sso", + "organizations/manage-sso": { + "newPath": "guides/organizations/sso-enterprise-connections", "action": "move" }, - "organizations/metadata.mdx": { - "newPath": "/organizations/metadata", + "organizations/metadata": { + "newPath": "guides/organizations/metadata", "action": "move" }, - "organizations/org-slugs-in-urls.mdx": { - "newPath": "/organizations/org-slugs-in-urls", - "action": "move" + "organizations/org-slugs-in-urls": { + "action": "delete" }, - "organizations/organization-workspaces.mdx": { - "newPath": "/dashboard/overview", + "organizations/organization-workspaces": { + "newPath": "guides/dashboard/overview", "action": "consolidate" }, - "organizations/overview.mdx": { - "newPath": "/organizations/overview", + "organizations/overview": { + "newPath": "guides/organizations/overview", "action": "move" }, - "organizations/roles-permissions.mdx": { - "newPath": "/organizations/roles-permissions", + "organizations/roles-permissions": { + "newPath": "guides/organizations/roles-and-permissions", "action": "move" }, - "organizations/verified-domains.mdx": { - "newPath": "/organizations/verified-domains", + "organizations/verified-domains": { + "newPath": "guides/organizations/verified-domains", "action": "move" }, - "testing/cypress/custom-commands.mdx": { - "newPath": "/development/testing/cypress/custom-commands", - "action": "move" + "testing/cypress/custom-commands": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/cypress/overview.mdx": { - "newPath": "/development/testing/cypress/overview", - "action": "move" + "testing/cypress/overview": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/cypress/test-account-portal.mdx": { - "newPath": "/development/testing/cypress/test-account-portal", - "action": "move" + "testing/cypress/test-account-portal": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/overview.mdx": { - "newPath": "/development/testing/overview", + "testing/overview": { + "newPath": "guides/development/testing", "action": "move" }, - "testing/playwright/overview.mdx": { - "newPath": "/development/testing/playwright/overview", - "action": "move" + "testing/playwright/overview": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/playwright/test-authenticated-flows.mdx": { - "newPath": "/development/testing/playwright/test-authenticated-flows", - "action": "move" + "testing/playwright/test-authenticated-flows": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/playwright/test-helpers.mdx": { - "newPath": "/development/testing/playwright/test-helpers", - "action": "move" + "testing/playwright/test-helpers": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/postman-or-insomnia.mdx": { - "newPath": "/development/testing/postman-or-insomnia", - "action": "move" + "testing/postman-or-insomnia": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "testing/test-emails-and-phones.mdx": { - "newPath": "/development/testing/test-emails-and-phones", - "action": "move" + "testing/test-emails-and-phones": { + "newPath": "guides/development/testing", + "action": "consolidate" }, - "troubleshooting/create-a-minimal-reproduction.mdx": { - "newPath": "/development/troubleshooting/create-a-minimal-reproduction", - "action": "move" + "troubleshooting/create-a-minimal-reproduction": { + "newPath": "guides/development/troubleshooting", + "action": "consolidate" }, - "troubleshooting/email-deliverability.mdx": { - "newPath": "/development/troubleshooting/email-deliverability", - "action": "move" + "troubleshooting/email-deliverability": { + "newPath": "guides/development/troubleshooting", + "action": "consolidate" }, - "troubleshooting/overview.mdx": { - "newPath": "/development/troubleshooting/overview", + "troubleshooting/overview": { + "newPath": "guides/development/troubleshooting", "action": "move" }, - "troubleshooting/script-loading.mdx": { - "newPath": "/development/troubleshooting/script-loading", - "action": "move" + "troubleshooting/script-loading": { + "newPath": "guides/development/troubleshooting", + "action": "consolidate" }, - "users/overview.mdx": { - "newPath": "/users/managing", + "users/overview": { + "newPath": "guides/users/managing", "action": "move" }, - "users/creating-users.mdx": { - "newPath": "/users/managing", + "users/creating-users": { + "newPath": "guides/users/managing", "action": "consolidate" }, - "users/deleting-users.mdx": { - "newPath": "/users/managing", - "action": "move" - }, - "users/invitations.mdx": { - "newPath": "/users/managing", + "users/deleting-users": { + "newPath": "guides/users/managing", "action": "consolidate" }, - "users/metadata.mdx": { - "newPath": "/users/managing", + "users/invitations": { + "newPath": "guides/users/managing", "action": "consolidate" }, - "users/user-impersonation.mdx": { - "newPath": "/users/impersonation", + "users/metadata": { + "newPath": "guides/users/extending", + "action": "move" + }, + "users/user-impersonation": { + "newPath": "guides/users/impersonation", "action": "move" }, - "security/bot-protection.mdx": { - "newPath": "/secure/bot-protection", + "security/bot-protection": { + "newPath": "guides/secure/bot-detection", "action": "move" }, - "security/csrf-protection.mdx": { - "newPath": "/secure/best-practices/csrf-protection", + "security/csrf-protection": { + "newPath": "guides/secure/best-practices/csrf-protection", "action": "move" }, - "security/clerk-csp.mdx": { - "newPath": "/secure/best-practices/csp-headers", + "security/clerk-csp": { + "newPath": "guides/secure/best-practices/csp-headers", "action": "move" }, - "security/email-link-protection.mdx": { - "newPath": "/secure/best-practices/email-link-protection", + "security/email-link-protection": { + "newPath": "guides/secure/best-practices/protect-email-link-sign-ins-and-sign-ups", "action": "move" }, - "security/xss-leak-protection.mdx": { - "newPath": "/secure/best-practices/xss-leak-protection", + "security/xss-leak-protection": { + "newPath": "guides/secure/best-practices/xss-leak-protection", "action": "move" }, - "security/fixation-protection.mdx": { - "newPath": "/secure/best-practices/fixation-protection", + "security/fixation-protection": { + "newPath": "guides/secure/best-practices/fixation-protection", "action": "move" }, - "security/vulnerability-disclosure-policy.mdx": { - "newPath": "/security/vulnerability-disclosure-policy", + "security/vulnerability-disclosure-policy": { + "newPath": "guides/how-clerk-works/security/vulnerability-disclosure-policy", "action": "move" }, - "security/password-protection.mdx": { - "newPath": "/security/password-protection", + "security/password-protection": { + "newPath": "guides/secure/password-protection-and-rules", "action": "move" }, - "telemetry.mdx": { - "newPath": "/security/telemetry", + "telemetry": { + "newPath": "guides/how-clerk-works/security/clerk-telemetry", "action": "move" }, - "security/user-lock-guide.mdx": { - "newPath": "/secure/brute-force-attacks", + "security/user-lock-guide": { + "newPath": "guides/secure/best-practices/brute-force-attacks-and-locking-user-accounts", "action": "move" }, - "security/unauthorized-sign-in.mdx": { - "newPath": "/secure/unauthorized-sign-in", + "security/unauthorized-sign-in": { + "newPath": "guides/secure/best-practices/unauthorized-sign-in", "action": "move" }, - "security/overview.mdx": { + "security/overview": { "action": "delete" }, - "maintenance-mode.mdx": { - "newPath": "maintenance-mode.mdx", + "maintenance-mode": { + "newPath": "maintenance-mode", "action": "move" }, - "versioning/available-versions.mdx": { - "newPath": "/development/versioning", + "versioning/available-versions": { + "newPath": "guides/development/upgrading/versioning", "action": "consolidate" }, - "versioning/overview.mdx": { - "newPath": "/development/versioning", + "versioning/overview": { + "newPath": "guides/development/upgrading/versioning", "action": "consolidate" } } diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 67b15dd94b..6587e56015 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -5,10 +5,24 @@ import { join, relative } from 'path' import { execSync } from 'child_process' import type { Manifest } from './generate-manifest' +/** + * Migration Assistant - Map Content Script + * + * This script validates content mapping configurations against the PROPOSAL manifest structure + * to ensure that planned migrations move docs in the direction of the proposed new structure. + * + * Key features: + * - Shows migration tasks needed for each mapping + * - Identifies invalid mappings (destinations not in proposal manifest) + * - Identifies new pages needed for proposal structure + * - Identifies unhandled legacy files + * - Can execute migration commands with --fix flag + */ + // Path to the docs directory and mapping file const MAPPING_PATH = join(process.cwd(), './proposal-mapping.json') const MANIFEST_PROPOSAL_PATH = join(process.cwd(), './public/manifest.proposal.json') -const MANIFEST_PATH = join(process.cwd(), './public/manifest.json') +const MANIFEST_PATH = join(process.cwd(), './docs/manifest.json') const DOCS_PATH = join(process.cwd(), './docs') // Environment variable to control output @@ -70,13 +84,20 @@ async function readMapping() { } > } catch (error) { - console.error(`❌ Error reading mapping.json: ${error.message}`) + console.error(`❌ Error reading mapping.json: ${error instanceof Error ? error.message : String(error)}`) return {} } } type Mapping = Awaited> +/** + * Convert href path to relative docs path + */ +function hrefToDocsPath(href: string): string { + return href.replace(/^\/docs\//, '') +} + /** * Read all manifest files and extract destination paths * @returns {Promise} Array of all destination paths from manifests @@ -87,7 +108,7 @@ async function readProposalManifestPaths() { return { manifest, paths: parseManifestPaths(manifest) } } -async function getManifestPaths() { +async function readManifestPaths() { const content = await readFile(MANIFEST_PATH, 'utf-8') const manifest = JSON.parse(content) as { navigation: Manifest } return { manifest, paths: parseManifestPaths(manifest) } @@ -101,7 +122,8 @@ function parseManifestPaths(manifest: { navigation: Manifest }) { for (const group of groups) { for (const item of group) { if ('href' in item) { - manifestPaths.push(item.href) + // Convert /docs/path to path (relative to docs folder) + manifestPaths.push(hrefToDocsPath(item.href)) } if ('items' in item) { @@ -120,19 +142,19 @@ function parseManifestPaths(manifest: { navigation: Manifest }) { * Recursively find all .mdx files in a directory * @param {string} dir - Directory to search * @param {string} baseDir - Base directory for relative paths - * @returns {Promise} Array of relative file paths + * @returns {Promise} Array of relative file paths without .mdx extension */ async function findMdxFiles(dir: string, baseDir = dir) { const files = await glob(`${dir}/**/*.mdx`, { ignore: ['**/_*/**'] }) - return files.map((file) => relative(baseDir, file)) + return files.map((file) => relative(baseDir, file).replace(/\.mdx$/, '')) } /** * Check if a file matches a destination glob pattern */ function matchesDestinationPattern(filePath: string, destPattern: string): boolean { - // Convert destination pattern to file format: /docs/path/** β†’ path/** - let pattern = destPattern.replace(/^\/docs\//, '').replace(/^\//, '') + // Convert destination pattern to file format + let pattern = destPattern // Convert glob pattern to regex for matching // Important: Replace ** first, then *, to avoid ** being overridden @@ -142,7 +164,7 @@ function matchesDestinationPattern(filePath: string, destPattern: string): boole .replace(/\*/g, '[^/]*') // * matches anything except / .replace(/___DOUBLESTAR___/g, '.*') // ** matches anything including / - return new RegExp(`^${regexPattern}$`).test(filePath) || new RegExp(`^${regexPattern}\\.mdx$`).test(filePath) + return new RegExp(`^${regexPattern}$`).test(filePath) } /** @@ -166,10 +188,7 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping, expandedMappin // Check if file matches destination pattern (means it's a successful migration) if (value.newPath && value.action !== 'drop' && value.action !== 'delete' && value.action !== 'deleted') { // Check exact match first - let destinationFile = value.newPath.replace(/^\/docs\//, '').replace(/^\//, '') - if (!destinationFile.endsWith('.mdx')) { - destinationFile += '.mdx' - } + let destinationFile = value.newPath if (file === destinationFile) { return false // This file is a successful migration } @@ -191,53 +210,56 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping, expandedMappin // These are files that exist in the new structure but ended up in slightly different locations // than intended during migration execution const possibleMisplacedFiles = [ - // Files that should have gone to /configure/ but ended up in /development/ - { pattern: /^development\/custom-session-token\.mdx$/, intended: 'configure/session-token.mdx' }, - { pattern: /^development\/jwt-templates\.mdx$/, intended: 'development/jwt-templates.mdx' }, // This one might be correct - { pattern: /^development\/making-requests\.mdx$/, intended: 'development/making-requests.mdx' }, // This one might be correct - { pattern: /^development\/manual-jwt\.mdx$/, intended: 'development/manual-jwt.mdx' }, // This one might be correct + // Files that should have gone to configure/ but ended up in development/ + { pattern: /^development\/custom-session-token$/, intended: 'guides/configure/session-token-customization' }, + { pattern: /^development\/jwt-templates$/, intended: 'guides/development/jwt-templates' }, // This one might be correct + { pattern: /^development\/making-requests$/, intended: 'guides/development/making-requests' }, // This one might be correct + { pattern: /^development\/manual-jwt$/, intended: 'guides/development/manual-jwt' }, // This one might be correct // Files that ended up in slightly different webhook locations - { pattern: /^configure\/webhooks\/debug-your-webhooks\.mdx$/, intended: 'configure/webhooks/debugging.mdx' }, - { pattern: /^configure\/webhooks\/sync-data\.mdx$/, intended: 'configure/webhooks/syncing.mdx' }, + { pattern: /^configure\/webhooks\/debug-your-webhooks$/, intended: 'guides/configure/webhooks/debugging' }, + { pattern: /^configure\/webhooks\/sync-data$/, intended: 'guides/configure/webhooks/syncing' }, // Files that ended up with slightly different proxy locations - { pattern: /^dashboard\/using-proxies\.mdx$/, intended: 'dashboard/proxy-fapi.mdx' }, + { pattern: /^dashboard\/using-proxies$/, intended: 'guides/dashboard/dns-domains/proxy-fapi' }, // Files in appearance-prop that should be in parent directories { - pattern: /^customizing-clerk\/appearance-prop\/organization-profile\.mdx$/, - intended: 'customizing-clerk/adding-items/organization-profile.mdx', + pattern: /^customizing-clerk\/appearance-prop\/organization-profile$/, + intended: 'guides/customizing-clerk/adding-items', }, { - pattern: /^customizing-clerk\/appearance-prop\/user-button\.mdx$/, - intended: 'customizing-clerk/adding-items/user-button.mdx', + pattern: /^customizing-clerk\/appearance-prop\/user-button$/, + intended: 'guides/customizing-clerk/adding-items', }, { - pattern: /^customizing-clerk\/appearance-prop\/user-profile\.mdx$/, - intended: 'customizing-clerk/adding-items/user-profile.mdx', + pattern: /^customizing-clerk\/appearance-prop\/user-profile$/, + intended: 'guides/customizing-clerk/adding-items', }, // Development/deployment files that are misplaced or consolidated - { pattern: /^development\/deployment\/exporting-users\.mdx$/, intended: 'development/migrating/overview.mdx' }, + { pattern: /^development\/deployment\/exporting-users$/, intended: 'guides/development/migrating/overview' }, + { + pattern: /^development\/deployment\/migrate-from-cognito$/, + intended: 'guides/development/migrating/migrate-from-cognito', + }, { - pattern: /^development\/deployment\/migrate-from-cognito\.mdx$/, - intended: 'development/migrating/migrate-from-cognito.mdx', + pattern: /^development\/deployment\/migrate-from-firebase$/, + intended: 'guides/development/migrating/migrate-from-firebase', }, + { pattern: /^development\/deployment\/migrate-overview$/, intended: 'guides/development/migrating/overview' }, { - pattern: /^development\/deployment\/migrate-from-firebase\.mdx$/, - intended: 'development/migrating/migrate-from-firebase.mdx', + pattern: /^development\/deployment\/overview$/, + intended: 'guides/development/deployment/deploy-to-production', }, - { pattern: /^development\/deployment\/migrate-overview\.mdx$/, intended: 'development/migrating/overview.mdx' }, - { pattern: /^development\/deployment\/overview\.mdx$/, intended: 'development/deployment/production.mdx' }, - { pattern: /^development\/overview\.mdx$/, intended: 'development/making-requests.mdx' }, // might be consolidated + { pattern: /^development\/overview$/, intended: 'guides/development/making-requests' }, // might be consolidated // Other common misplaced patterns from the unhandled list { - pattern: /^customizing-clerk\/appearance-prop\/localization\.mdx$/, - intended: 'customizing-clerk/localization.mdx', + pattern: /^customizing-clerk\/appearance-prop\/localization$/, + intended: 'guides/customizing-clerk/localization', }, - { pattern: /^secure\/.*\.mdx$/, intended: '' }, // Many files moved to secure/ directory + { pattern: /^secure\/.*$/, intended: '' }, // Many files moved to secure/ directory ] for (const { pattern, intended } of possibleMisplacedFiles) { @@ -250,26 +272,6 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping, expandedMappin }) } -/** - * Find pages that need to be created (in manifest but no mapping source) - * @param manifestPaths - All paths from manifest files - * @param mapping - Mapping configuration - * @returns Array of paths that need new content - */ -function findPagesToCreate(manifestPaths: string[], mapping: Mapping) { - const mappedDestinations = new Set(Object.values(mapping).map((entry) => entry.newPath)) - - // Also collect manifest paths that are explicitly marked for dropping - const droppedPaths = new Set() - for (const [key, value] of Object.entries(mapping)) { - if (value.action === 'drop' && key.startsWith('/docs/')) { - droppedPaths.add(key) - } - } - - return manifestPaths.filter((path) => !mappedDestinations.has(path) && !droppedPaths.has(path)) -} - /** * Display warnings for unhandled files and pages to create * @param {string[]} unhandledFiles - Array of unhandled file paths @@ -313,13 +315,14 @@ function displayWarnings( hasWarnings = true if (unhandledFiles.length > 0) console.log('\n' + '─'.repeat(60) + '\n') - console.log(`❌ Found ${invalidMappings.length} invalid mappings (destinations not in manifests):\n`) + console.log(`❌ Found ${invalidMappings.length} invalid mappings (destinations not in proposal manifest):\n`) // Group by destination directory for better readability const groupedMappings: Record = {} invalidMappings.forEach((mapping) => { + if (!mapping.destination) return // Skip mappings without destinations const parts = mapping.destination.split('/') - const section = parts.length > 2 ? parts[2] : 'root' // /docs/[section]/... + const section = parts.length > 1 ? parts[1] : 'root' // guides/[section]/... if (!groupedMappings[section]) groupedMappings[section] = [] groupedMappings[section].push(mapping) }) @@ -340,13 +343,13 @@ function displayWarnings( hasWarnings = true if (unhandledFiles.length > 0 || invalidMappings.length > 0) console.log('\n' + '─'.repeat(60) + '\n') - console.log(`πŸ“ Found ${pagesToCreate.length} pages that need new content:\n`) + console.log(`πŸ“ Found ${pagesToCreate.length} pages that need new content in proposal:\n`) // Group by section for better readability const groupedPages: Record = {} pagesToCreate.forEach((path) => { const parts = path.split('/') - const section = parts.length > 2 ? parts[2] : 'root' // /docs/[section]/... + const section = parts.length > 1 ? parts[1] : 'root' // guides/[section]/... if (!groupedPages[section]) groupedPages[section] = [] groupedPages[section].push(path) }) @@ -364,7 +367,9 @@ function displayWarnings( } if (!hasWarnings) { - console.log('βœ… All legacy files are handled, all mappings are valid, and all manifest pages have content sources') + console.log( + 'βœ… All legacy files are handled, all mappings are valid against proposal, and all proposal pages have content sources', + ) return } @@ -377,7 +382,7 @@ function displayWarnings( const generateFiles = Object.values(expandedMapping).filter((mapping) => mapping.action === 'generate').length console.log( - `\nπŸ“Š Summary: ${unhandledFiles.length} unhandled files, ${invalidMappings.length} invalid mappings, ${pagesToCreate.length} pages need new content, ${convertToExampleFiles} convert to examples, ${generateFiles} pages need to be generated, ${Object.keys(expandedMapping).length} files mapped`, + `\nπŸ“Š Summary: ${unhandledFiles.length} unhandled files, ${invalidMappings.length} invalid mappings (against proposal), ${pagesToCreate.length} proposal pages need new content, ${convertToExampleFiles} convert to examples, ${generateFiles} pages need to be generated, ${Object.keys(expandedMapping).length} files mapped`, ) } @@ -428,7 +433,7 @@ function applyGlobMapping(filePath: string, globPattern: string, newPathPattern: if (filePath.startsWith(globPrefix)) { const matchedPart = filePath.slice(globPrefix.length) - return newPathPrefix + matchedPart.replace(/\.mdx$/, '') + newPathSuffix + return newPathPrefix + matchedPart + newPathSuffix } } @@ -481,13 +486,13 @@ function expandGlobMapping(mapping: Mapping, allFiles: string[]) { } /** - * Find invalid mapping destinations (mapped to paths that don't exist in manifests) + * Find invalid mapping destinations (mapped to paths that don't exist in proposal manifests) * @param {Object} expandedMapping - Expanded mapping configuration - * @param {string[]} manifestPaths - All paths from manifest files + * @param {string[]} proposalManifestPaths - All paths from proposal manifest files * @returns {string[]} Array of invalid mapping destinations */ -function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) { - const manifestPathsSet = new Set(manifestPaths) +function findInvalidMappings(expandedMapping: Mapping, proposalManifestPaths: string[]) { + const proposalPathsSet = new Set(proposalManifestPaths) const invalidMappings: Array<{ source: string; destination: string; action: string }> = [] for (const [sourcePath, mapping] of Object.entries(expandedMapping)) { @@ -498,7 +503,7 @@ function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) continue } - if (!manifestPathsSet.has(mapping.newPath)) { + if (!proposalPathsSet.has(mapping.newPath)) { invalidMappings.push({ source: sourcePath, destination: mapping.newPath, @@ -512,6 +517,26 @@ function findInvalidMappings(expandedMapping: Mapping, manifestPaths: string[]) type InvalidMapping = Awaited> +/** + * Find pages that need to be created (in proposal manifest but no mapping source) + * @param proposalManifestPaths - All paths from proposal manifest files + * @param mapping - Mapping configuration + * @returns Array of paths that need new content + */ +function findPagesToCreate(proposalManifestPaths: string[], mapping: Mapping) { + const mappedDestinations = new Set(Object.values(mapping).map((entry) => entry.newPath)) + + // Also collect proposal manifest paths that are explicitly marked for dropping + const droppedPaths = new Set() + for (const [key, value] of Object.entries(mapping)) { + if (value.action === 'drop') { + droppedPaths.add(key) + } + } + + return proposalManifestPaths.filter((path) => !mappedDestinations.has(path) && !droppedPaths.has(path)) +} + /** * Display only unhandled files * @param {string[]} unhandledFiles - Array of unhandled file paths @@ -562,7 +587,7 @@ async function fileExists(filePath: string): Promise { * Get icon for action type */ function getActionIcon(action: string): string { - const icons = { + const icons: Record = { move: 'πŸ“', consolidate: 'πŸ”€', generate: '✨', @@ -594,10 +619,7 @@ function consolidateMoveTasks(moveTasks: Array<{ source: string; destination: st moveTasks.forEach((task) => { // Find the common directory pattern const sourceParts = task.source.split('/') - const destParts = task.destination - .replace(/^\/docs\//, '') - .split('/') - .filter((p) => p) // Remove empty parts + const destParts = task.destination.split('/').filter((p) => p) // Remove empty parts // Look for directory-level patterns (at least 2 files in same directory) if (sourceParts.length >= 2) { @@ -627,9 +649,8 @@ function consolidateMoveTasks(moveTasks: Array<{ source: string; destination: st // Check if all files in this group follow the same pattern const allSamePattern = files.every((file) => { const expectedSource = `${sourceBase}/${file.source.split('/').pop()}` - const destWithoutDocs = file.destination.replace(/^\/docs\//, '').replace(/^\//, '') - const expectedDest = `${destBase}/${destWithoutDocs.split('/').pop()}` - return file.source === expectedSource && destWithoutDocs === expectedDest + const expectedDest = `${destBase}/${file.destination.split('/').pop()}` + return file.source === expectedSource && file.destination === expectedDest }) if (allSamePattern) { @@ -683,7 +704,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ let skippedCount = 0 for (const [source, mapping] of Object.entries(expandedMapping)) { - const sourcePath = join(DOCS_PATH, source) + const sourcePath = join(DOCS_PATH, source + '.mdx') // Skip checking file existence for actions without destination paths if ( @@ -697,12 +718,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ continue } - // Construct destination path - remove leading slash and add .mdx extension - let destinationRelativePath = mapping.newPath.replace(/^\//, '') - if (!destinationRelativePath.endsWith('.mdx')) { - destinationRelativePath += '.mdx' - } - const destinationPath = join(DOCS_PATH, destinationRelativePath) + // Construct destination path + const destinationPath = join(DOCS_PATH, mapping.newPath + '.mdx') // Check if task is already completed (source doesn't exist but destination does) const sourceExists = await fileExists(sourcePath) @@ -772,7 +789,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ consolidationGroups.forEach((group) => { const paddedNumber = taskNumber.toString().padStart(3, '0') - console.log(`${paddedNumber}. πŸ”€ [CONSOLIDATE] Create guide: ${group.destination}`) + console.log(`${paddedNumber}. πŸ”€ [CONSOLIDATE] Create guide: /docs/${group.destination}`) console.log(` Consolidate content from ${group.count} files:`) group.sources.forEach((source) => { console.log(` β€’ ${source}`) @@ -796,11 +813,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ const filteredTasks = allTasks.filter((task) => { if (task.action === 'move') { // Normalize paths for comparison - const sourcePath = task.source.replace(/\.mdx$/, '') + const sourcePath = task.source const destPath = task.destination - .replace(/^\/docs\//, '') - .replace(/^\//, '') - .replace(/\.mdx$/, '') // Skip if source and destination are the same if (sourcePath === destPath) { @@ -829,10 +843,8 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ // Show the executable command if available or execute it if (task.action === 'move') { - const sourcePath = `/docs/${task.source.replace(/\.mdx$/, '')}` - const destPath = task.destination.startsWith('/docs/') - ? task.destination.replace(/\.mdx$/, '') - : `/docs${task.destination.replace(/\.mdx$/, '')}` + const sourcePath = `/docs/${task.source}` + const destPath = `/docs/${task.destination}` const command = `node scripts/move-doc.mjs ${sourcePath} ${destPath}` if (FIX_MODE) { @@ -848,7 +860,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(` Command: ${command}`) } } else if (task.action === 'delete') { - const docPath = `/docs/${task.source.replace(/\.mdx$/, '')}` + const docPath = `/docs/${task.source}` const command = `node scripts/delete-doc.mjs ${docPath}` if (FIX_MODE) { @@ -976,7 +988,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ * Main function to parse docs content and show mapping actions */ async function main() { - const [mdxFiles, mapping, { paths: manifestPaths }] = await Promise.all([ + const [mdxFiles, mapping, { paths: proposalManifestPaths }] = await Promise.all([ findMdxFiles(DOCS_PATH), readMapping(), readProposalManifestPaths(), @@ -995,15 +1007,18 @@ async function main() { // Show what actions are needed for each mapping await displayMappingActions(expandedMapping, mdxFiles) - // Show unhandled files + // Find invalid mappings (against proposal manifest) + const invalidMappings = findInvalidMappings(expandedMapping, proposalManifestPaths) + + // Find pages that need to be created (from proposal manifest) + const pagesToCreate = findPagesToCreate(proposalManifestPaths, mapping) + + // Show unhandled files and validation results const unhandledFiles = findUnhandledFiles(mdxFiles, mapping, expandedMapping) - if (unhandledFiles.length > 0) { - console.log(`\n⚠️ UNHANDLED LEGACY FILES (${unhandledFiles.length} files):`) - console.log(` These files exist but have no mapping defined:\n`) - unhandledFiles.sort().forEach((file) => { - console.log(` β€’ ${file}`) - }) - } + + // Show warnings section + console.log('\n' + '═'.repeat(80) + '\n') + displayWarnings(unhandledFiles, pagesToCreate, expandedMapping, invalidMappings) if (!FIX_MODE) { console.log('\nπŸ’‘ Usage options:') From c3348ad1bbdbaf24fb3caa51c6155d778d549343 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 29 Jul 2025 22:55:41 +0800 Subject: [PATCH 040/136] Remove --fix arg from script --- scripts/migration-assistant/map-content.ts | 139 +++------------------ 1 file changed, 16 insertions(+), 123 deletions(-) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index 6587e56015..ca55198a85 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -2,7 +2,6 @@ import 'dotenv/config' import { readFile, access } from 'fs/promises' import { glob } from 'glob' import { join, relative } from 'path' -import { execSync } from 'child_process' import type { Manifest } from './generate-manifest' /** @@ -28,38 +27,6 @@ const DOCS_PATH = join(process.cwd(), './docs') // Environment variable to control output const WARNINGS_ONLY = process.env.DOCS_WARNINGS_ONLY === 'true' -// Command line arguments -const FIX_MODE = process.argv.includes('--fix') - -/** - * Execute a command and return success/failure - */ -async function executeCommand(command: string, description: string): Promise<{ success: boolean; error?: string }> { - try { - console.log(`πŸ”„ Executing: ${description}`) - console.log(` Command: ${command}`) - - execSync(command, { stdio: ['inherit', 'inherit', 'pipe'], cwd: process.cwd() }) - - console.log(`βœ… Success: ${description}`) - return { success: true } - } catch (error: any) { - let errorMessage = 'Unknown error' - - if (error?.stderr) { - errorMessage = error.stderr.toString().trim() - } else if (error?.stdout) { - errorMessage = error.stdout.toString().trim() - } else if (error?.message) { - errorMessage = error.message - } - - console.log(`❌ Failed: ${description}`) - console.log(` Error: ${errorMessage}`) - return { success: false, error: errorMessage } - } -} - /** * Read and parse the mapping.json file * @returns {Promise} Parsed mapping object @@ -739,7 +706,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ } } - console.log(`πŸ“‹ PROPOSAL MAPPING TASKS${FIX_MODE ? ' (--fix mode: executing commands)' : ''}:\n`) + console.log(`πŸ“‹ PROPOSAL MAPPING TASKS:\n`) // Consolidate move tasks into patterns const { patterns, individual: individualMoves } = consolidateMoveTasks(moveTasks) @@ -748,10 +715,6 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ const consolidationGroups = groupConsolidationTasks(consolidateTasks) let taskNumber = 1 - const executionResults = { - batchMoves: { successful: 0, failed: 0, errors: [] as string[] }, - individualMoves: { successful: 0, failed: 0, errors: [] as string[] }, - } // Show consolidated move patterns first if (patterns.length > 0) { @@ -763,19 +726,9 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ `${paddedNumber}. πŸ“ [BATCH MOVE] ${pattern.files.length} files: ${pattern.pattern} β†’ ${pattern.destPattern}`, ) - if (FIX_MODE) { - const result = await executeCommand(pattern.command, `Batch move ${pattern.files.length} files`) - if (result.success) { - executionResults.batchMoves.successful++ - } else { - executionResults.batchMoves.failed++ - executionResults.batchMoves.errors.push(`${pattern.pattern}: ${result.error}`) - } - } else { - console.log(` Batch move ${pattern.files.length} files using glob patterns`) - console.log(` Command: ${pattern.command}`) - console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) - } + console.log(` Batch move ${pattern.files.length} files using glob patterns`) + console.log(` Command: ${pattern.command}`) + console.log(` Files: ${pattern.files.map((f) => f.source).join(', ')}`) console.log('') taskNumber++ } @@ -794,9 +747,7 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ group.sources.forEach((source) => { console.log(` β€’ ${source}`) }) - if (FIX_MODE) { - console.log(` ⚠️ Manual task: Cannot be automated`) - } + console.log(` ⚠️ Manual task: Cannot be automated`) console.log('') taskNumber++ }) @@ -841,41 +792,25 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ console.log(`${paddedNumber}. ${icon} [${task.action.toUpperCase()}] ${task.source} β†’ ${task.destination}`) } - // Show the executable command if available or execute it + // Show the executable command if (task.action === 'move') { const sourcePath = `/docs/${task.source}` const destPath = `/docs/${task.destination}` const command = `node scripts/move-doc.mjs ${sourcePath} ${destPath}` - if (FIX_MODE) { - const result = await executeCommand(command, `Move ${task.source}`) - if (result.success) { - executionResults.individualMoves.successful++ - } else { - executionResults.individualMoves.failed++ - executionResults.individualMoves.errors.push(`${task.source}: ${result.error}`) - } - } else { - console.log(` Move file, update manifest links, update internal links, add redirect`) - console.log(` Command: ${command}`) - } + console.log(` Move file, update manifest links, update internal links, add redirect`) + console.log(` Command: ${command}`) } else if (task.action === 'delete') { const docPath = `/docs/${task.source}` const command = `node scripts/delete-doc.mjs ${docPath}` - if (FIX_MODE) { - console.log(` ⚠️ Manual task: Use delete-doc.mjs script (checks for references)`) - console.log(` Command: ${command}`) - } else { - console.log(` Check for references, remove from manifest, add redirect, delete file`) - console.log(` Command: ${command}`) - } + console.log(` Check for references, remove from manifest, add redirect, delete file`) + console.log(` Command: ${command}`) } else if ( - FIX_MODE && - (task.action === 'move-to-examples' || - task.action === 'TODO' || - task.action === 'deleted' || - task.action === 'drop') + task.action === 'move-to-examples' || + task.action === 'TODO' || + task.action === 'deleted' || + task.action === 'drop' ) { console.log(` ⚠️ Manual task: Cannot be automated`) } @@ -943,45 +878,6 @@ async function displayMappingActions(expandedMapping: Mapping, mdxFiles: string[ ` β€’ Consolidation creates ${consolidationGroups.length} guides from ${consolidationFiles} source files!`, ) } - - // Show execution results if in fix mode - if (FIX_MODE) { - console.log(`\nπŸ”§ EXECUTION RESULTS:`) - - const totalSuccessful = executionResults.batchMoves.successful + executionResults.individualMoves.successful - const totalFailed = executionResults.batchMoves.failed + executionResults.individualMoves.failed - const totalExecuted = totalSuccessful + totalFailed - - console.log(` β€’ Commands executed: ${totalExecuted}`) - console.log(` β€’ Successful: ${totalSuccessful}`) - console.log(` β€’ Failed: ${totalFailed}`) - - if (executionResults.batchMoves.successful > 0 || executionResults.batchMoves.failed > 0) { - console.log( - ` β€’ Batch moves: ${executionResults.batchMoves.successful} successful, ${executionResults.batchMoves.failed} failed`, - ) - } - if (executionResults.individualMoves.successful > 0 || executionResults.individualMoves.failed > 0) { - console.log( - ` β€’ Individual moves: ${executionResults.individualMoves.successful} successful, ${executionResults.individualMoves.failed} failed`, - ) - } - // Show errors if any - const allErrors = [...executionResults.batchMoves.errors, ...executionResults.individualMoves.errors] - - if (allErrors.length > 0) { - console.log(`\n❌ ERRORS:`) - allErrors.forEach((error) => { - console.log(` β€’ ${error}`) - }) - } - - if (totalFailed === 0) { - console.log(`\nπŸŽ‰ All automated tasks completed successfully!`) - } else { - console.log(`\n⚠️ Some tasks failed. Please review the errors above and retry if needed.`) - } - } } /** @@ -1020,11 +916,8 @@ async function main() { console.log('\n' + '═'.repeat(80) + '\n') displayWarnings(unhandledFiles, pagesToCreate, expandedMapping, invalidMappings) - if (!FIX_MODE) { - console.log('\nπŸ’‘ Usage options:') - console.log(' β€’ Run with DOCS_WARNINGS_ONLY=true to see only unhandled files') - console.log(' β€’ Run with --fix to automatically execute all automated commands') - } + console.log('\nπŸ’‘ Usage options:') + console.log(' β€’ Run with DOCS_WARNINGS_ONLY=true to see only unhandled files') } // Run the script From 7e0544126aba0501c708910921e1cc68bde47c32 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 29 Jul 2025 23:09:16 +0800 Subject: [PATCH 041/136] Remove temporary fix for files in map-content.ts --- scripts/migration-assistant/map-content.ts | 62 ---------------------- 1 file changed, 62 deletions(-) diff --git a/scripts/migration-assistant/map-content.ts b/scripts/migration-assistant/map-content.ts index ca55198a85..e9da16ae57 100644 --- a/scripts/migration-assistant/map-content.ts +++ b/scripts/migration-assistant/map-content.ts @@ -173,68 +173,6 @@ function findUnhandledFiles(allFiles: string[], mapping: Mapping, expandedMappin } } - // TEMPORARY FIX: Check if this file appears to be a "misplaced" migration result - // These are files that exist in the new structure but ended up in slightly different locations - // than intended during migration execution - const possibleMisplacedFiles = [ - // Files that should have gone to configure/ but ended up in development/ - { pattern: /^development\/custom-session-token$/, intended: 'guides/configure/session-token-customization' }, - { pattern: /^development\/jwt-templates$/, intended: 'guides/development/jwt-templates' }, // This one might be correct - { pattern: /^development\/making-requests$/, intended: 'guides/development/making-requests' }, // This one might be correct - { pattern: /^development\/manual-jwt$/, intended: 'guides/development/manual-jwt' }, // This one might be correct - - // Files that ended up in slightly different webhook locations - { pattern: /^configure\/webhooks\/debug-your-webhooks$/, intended: 'guides/configure/webhooks/debugging' }, - { pattern: /^configure\/webhooks\/sync-data$/, intended: 'guides/configure/webhooks/syncing' }, - - // Files that ended up with slightly different proxy locations - { pattern: /^dashboard\/using-proxies$/, intended: 'guides/dashboard/dns-domains/proxy-fapi' }, - - // Files in appearance-prop that should be in parent directories - { - pattern: /^customizing-clerk\/appearance-prop\/organization-profile$/, - intended: 'guides/customizing-clerk/adding-items', - }, - { - pattern: /^customizing-clerk\/appearance-prop\/user-button$/, - intended: 'guides/customizing-clerk/adding-items', - }, - { - pattern: /^customizing-clerk\/appearance-prop\/user-profile$/, - intended: 'guides/customizing-clerk/adding-items', - }, - - // Development/deployment files that are misplaced or consolidated - { pattern: /^development\/deployment\/exporting-users$/, intended: 'guides/development/migrating/overview' }, - { - pattern: /^development\/deployment\/migrate-from-cognito$/, - intended: 'guides/development/migrating/migrate-from-cognito', - }, - { - pattern: /^development\/deployment\/migrate-from-firebase$/, - intended: 'guides/development/migrating/migrate-from-firebase', - }, - { pattern: /^development\/deployment\/migrate-overview$/, intended: 'guides/development/migrating/overview' }, - { - pattern: /^development\/deployment\/overview$/, - intended: 'guides/development/deployment/deploy-to-production', - }, - { pattern: /^development\/overview$/, intended: 'guides/development/making-requests' }, // might be consolidated - - // Other common misplaced patterns from the unhandled list - { - pattern: /^customizing-clerk\/appearance-prop\/localization$/, - intended: 'guides/customizing-clerk/localization', - }, - { pattern: /^secure\/.*$/, intended: '' }, // Many files moved to secure/ directory - ] - - for (const { pattern, intended } of possibleMisplacedFiles) { - if (pattern.test(file)) { - return false // Treat misplaced files as handled to avoid false positives - } - } - return true // This file is truly unhandled }) } From d231049779146ee81e9f3b1595f9cb16fe57d7f1 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:54:52 -0400 Subject: [PATCH 042/136] manual fixes --- proposal-mapping.json | 86 +++++++++++++++++++++---------------------- proposal.md | 54 +++++++++++++++++---------- 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/proposal-mapping.json b/proposal-mapping.json index e4976d0669..bf99762568 100644 --- a/proposal-mapping.json +++ b/proposal-mapping.json @@ -110,7 +110,7 @@ "action": "move" }, "authentication/configuration/sign-up-sign-in-options": { - "newPath": "guides/configure/auth-strategies/sign-up-and-sign-in-options", + "newPath": "guides/configure/auth-strategies/sign-up-sign-in-options", "action": "move" }, "authentication/configuration/force-mfa": { @@ -169,7 +169,7 @@ "action": "move" }, "customization/themes": { - "newPath": "guides/customizing-clerk/appearance-prop/theme", + "newPath": "guides/customizing-clerk/appearance-prop/themes", "action": "move" }, "customization/variables": { @@ -181,15 +181,15 @@ "action": "move" }, "customization/organization-profile": { - "newPath": "guides/customizing-clerk/adding-items", + "newPath": "guides/customizing-clerk/adding-items/organization-profile", "action": "move" }, "customization/user-button": { - "newPath": "guides/customizing-clerk/adding-items", + "newPath": "guides/customizing-clerk/adding-items/user-button", "action": "move" }, "customization/user-profile": { - "newPath": "guides/customizing-clerk/adding-items", + "newPath": "guides/customizing-clerk/adding-items/user-profile", "action": "move" }, "customization/localization": { @@ -237,15 +237,16 @@ "action": "move" }, "deployments/set-up-preview-environment": { - "newPath": "guides/development/deployment/set-up-a-preview-environment", - "action": "move" + "newPath": "guides/development/deployment/managing-environments", + "action": "consolidate" }, "deployments/set-up-staging": { - "newPath": "guides/development/deployment/set-up-a-staging-environment", - "action": "move" + "newPath": "guides/development/deployment/managing-environments", + "action": "consolidate" }, "deployments/staging-alternatives": { - "action": "delete" + "newPath": "guides/development/deployment/staging-alternatives", + "action": "move" }, "deployments/migrate-overview": { "newPath": "guides/development/migrating/overview", @@ -256,11 +257,11 @@ "action": "consolidate" }, "deployments/migrate-from-cognito": { - "newPath": "guides/development/migrating/migrate-from-cognito", + "newPath": "guides/development/migrating/cognito", "action": "move" }, "deployments/migrate-from-firebase": { - "newPath": "guides/development/migrating/migrate-from-firebase", + "newPath": "guides/development/migrating/firebase", "action": "move" }, "errors/overview": { @@ -291,7 +292,7 @@ "action": "delete" }, "guides/reverification": { - "newPath": "guides/secure/re-verification", + "newPath": "guides/secure/reverification", "action": "move" }, "guides/routing": { @@ -311,8 +312,7 @@ "action": "move" }, "organizations/create-orgs-for-users": { - "newPath": "guides/organizations/managing-orgs", - "action": "move" + "action": "TODO" }, "organizations/force-organizations": { "action": "delete" @@ -330,7 +330,7 @@ "action": "move" }, "organizations/org-slugs-in-urls": { - "action": "delete" + "action": "TODO" }, "organizations/organization-workspaces": { "newPath": "guides/dashboard/overview", @@ -349,56 +349,56 @@ "action": "move" }, "testing/cypress/custom-commands": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/cypress/custom-commands", + "action": "move" }, "testing/cypress/overview": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/cypress/overview", + "action": "move" }, "testing/cypress/test-account-portal": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/cypress/test-account-portal", + "action": "move" }, "testing/overview": { - "newPath": "guides/development/testing", + "newPath": "guides/development/testing/overview", "action": "move" }, "testing/playwright/overview": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/playwright/overview", + "action": "move" }, "testing/playwright/test-authenticated-flows": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/playwright/test-authenticated-flows", + "action": "move" }, "testing/playwright/test-helpers": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/playwright/test-helpers", + "action": "move" }, "testing/postman-or-insomnia": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/postman-or-insomnia", + "action": "move" }, "testing/test-emails-and-phones": { - "newPath": "guides/development/testing", - "action": "consolidate" + "newPath": "guides/development/testing/test-emails-and-phones", + "action": "move" }, "troubleshooting/create-a-minimal-reproduction": { - "newPath": "guides/development/troubleshooting", - "action": "consolidate" + "newPath": "guides/development/troubleshooting/create-a-minimal-reproduction", + "action": "move" }, "troubleshooting/email-deliverability": { - "newPath": "guides/development/troubleshooting", - "action": "consolidate" + "newPath": "guides/development/troubleshooting/email-deliverability", + "action": "move" }, "troubleshooting/overview": { - "newPath": "guides/development/troubleshooting", + "newPath": "guides/development/troubleshooting/overview", "action": "move" }, "troubleshooting/script-loading": { - "newPath": "guides/development/troubleshooting", - "action": "consolidate" + "newPath": "guides/development/troubleshooting/script-loading", + "action": "move" }, "users/overview": { "newPath": "guides/users/managing", @@ -425,7 +425,7 @@ "action": "move" }, "security/bot-protection": { - "newPath": "guides/secure/bot-detection", + "newPath": "guides/secure/bot-protection", "action": "move" }, "security/csrf-protection": { @@ -437,7 +437,7 @@ "action": "move" }, "security/email-link-protection": { - "newPath": "guides/secure/best-practices/protect-email-link-sign-ins-and-sign-ups", + "newPath": "guides/secure/best-practices/protect-email-links", "action": "move" }, "security/xss-leak-protection": { @@ -461,7 +461,7 @@ "action": "move" }, "security/user-lock-guide": { - "newPath": "guides/secure/best-practices/brute-force-attacks-and-locking-user-accounts", + "newPath": "guides/secure/best-practices/user-lockout", "action": "move" }, "security/unauthorized-sign-in": { diff --git a/proposal.md b/proposal.md index 851cb9dce4..5e0c35f9d2 100644 --- a/proposal.md +++ b/proposal.md @@ -36,7 +36,7 @@ ### Configuring your App [configure] - Authentication Strategies [auth-strategies] - - Sign-up and sign-in options + - Sign-up and sign-in options [sign-up-sign-in-options] - Social Connections - Apple - Atlassian @@ -124,11 +124,14 @@ - UI Customization (Appearance Prop) [appearance-prop] - Overview - Layout - - Theme + - Themes - Variables - CAPTCHA - Account Portal - Adding items to UI Components [adding-items] + - Organization profile + - User profile + - User button - Email and SMS Templates - Localization (i18n) [localization] - Clerk Elements (beta) [elements] @@ -154,10 +157,10 @@ - Restricting Access - Multifactor Authentication (MFA) [mfa] - Authorization Checks -- Bot Detection +- Bot Protection - Banning Users -- Prevent brute force attacks -- Re-verification (Step-up) +- Prevent brute force attacks [user-lockout] +- Reverification (Step-up) - Legal Compliance - Password protection and rules - Security Best Practices [best-practices] @@ -165,9 +168,7 @@ - CSRF protection - CSP Headers - Fixation protection - - Brute force attacks and locking user accounts - - Protect sign ups from bots - - Protect email link sign-ins and sign-ups + - Protect email link sign-ups and sign-ins [protect-email-links] - Unauthorized sign-in - Session Options @@ -179,22 +180,38 @@ - Override Clerk Types/Interfaces - Image Optimization - Testing with Clerk [testing] + - Overview + - Test emails and phones + - Cypress + - Overview + - Custom commands + - Test Account Portal + - Playwright + - Overview + - Test helpers + - Test authenticated flows + - Postman or Insomnia - Errors - Troubleshooting + - Overview + - Email deliverability + - Script loading + - Help & Support + - Create a minimal reproduction + - Community Discord + - Contact Support - Deployment - Changing domains - - Deploy to production - - Deploy to Vercel - - Deploy behind a proxy - - Deploy an Astro app to production - - Deploy a Chrome Extension to production - - Deploy an Expo app to production - - Set up a staging environment - - Set up a preview environment + - Deploy to production [production] + - Deploy to Vercel [vercel] + - Deploy behind a proxy [behind-a-proxy] + - Deploy an Astro app to production [astro] + - Deploy a Chrome Extension to production [chrome-extension] + - Deploy an Expo app to production [expo] - Migrating your Data [migrating] - Overview - - Migrate from Firebase - - Migrate from Cognito + - Migrate from Firebase [firebase] + - Migrate from Cognito [cognito] - SDK Development - Upgrading Clerk [upgrading] - Versioning & LTS [versioning] @@ -225,7 +242,6 @@ - Routing - Session tokens - Tokens & signatures -- Clerk environment variables - Security at Clerk [security] - Vulnerability disclosure policy - Clerk Telemetry From 4f48151363a91137322008e03417f3bd424cd827 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:16:05 -0400 Subject: [PATCH 043/136] update delete-doc script to update links for a deleted path --- scripts/delete-doc.mjs | 99 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/scripts/delete-doc.mjs b/scripts/delete-doc.mjs index 70efcf1c6e..2d5f74da8d 100644 --- a/scripts/delete-doc.mjs +++ b/scripts/delete-doc.mjs @@ -2,7 +2,7 @@ * Deletes MDX files and cleans up references. * * At a high level, the script does the following in the /clerk-docs repo: - * 1. Checks if any other MDX files link to the file to be deleted (fails if found) + * 1. Updates any links in MDX files that point to the deleted path * 2. Removes the file from the manifest.json * 3. Adds a redirect to the redirects/static/docs.json file * 4. Deletes the MDX file @@ -100,6 +100,78 @@ const checkMdxReferences = async (targetPath) => { return referencingFiles } +const updateMdxLinks = async (deletedPath, newPath) => { + const processFile = async (filePath) => { + const content = await fs.readFile(filePath, 'utf-8') + let updatedContent = content + + const { path: oldBasePath } = splitPathAndHash(deletedPath) + const { path: newBasePath, hash: newHash } = splitPathAndHash(newPath) + + // 1. Update markdown links + const markdownLinkRegex = new RegExp(`\\[([^\\]]+)\\]\\(${oldBasePath}(?:#[^)]*)?\\)`, 'g') + updatedContent = updatedContent.replace(markdownLinkRegex, (match, linkText) => { + const existingHash = match.match(/#[^)]*(?=\))/)?.[0] || '' + if (existingHash) { + console.log( + `⚠️ Hash found in markdown link: ${existingHash}. Ensure that the new path has the same hash, or update the hash accordingly.`, + ) + } + const finalHash = newHash || existingHash || '' + return `[${linkText}](${newBasePath}${finalHash})` + }) + + // 2. Update JSX/TSX component link props + // This regex looks for link="..." or link='...' patterns, being careful about quotes + const jsxLinkRegex = new RegExp(`(link=["'])(${oldBasePath}(?:#[^"']*)?)(["'])`, 'g') + updatedContent = updatedContent.replace(jsxLinkRegex, (match, prefix, linkPath, suffix) => { + const { hash: linkHash } = splitPathAndHash(linkPath) + if (linkHash) { + console.log( + `⚠️ Hash found in JSX link: ${linkHash}. Ensure that the new path has the same hash, or update the hash accordingly.`, + ) + } + const finalHash = newHash || linkHash || '' + return `${prefix}${newBasePath}${finalHash}${suffix}` + }) + + // 3. Update link prop in arrays + const arrayLinkRegex = new RegExp(`(link:\\s*["'])(${oldBasePath}(?:#[^"']*)?)(["'])`, 'g') + updatedContent = updatedContent.replace(arrayLinkRegex, (match, prefix, linkPath, suffix) => { + const { hash: linkHash } = splitPathAndHash(linkPath) + if (linkHash) { + console.log( + `⚠️ Hash found in array link: ${linkHash}. Ensure that the new path has the same hash, or update the hash accordingly.`, + ) + } + const finalHash = newHash || linkHash || '' + return `${prefix}${newBasePath}${finalHash}${suffix}` + }) + + if (content !== updatedContent) { + await fs.writeFile(filePath, updatedContent) + console.log(`Updated links in ${filePath}`) + } + } + + // Recursively process all MDX files + const processDirectory = async (dir) => { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + await processDirectory(fullPath) + } else if (entry.name.endsWith('.mdx')) { + await processFile(fullPath) + } + } + } + + await processDirectory(DOCS_DIR) +} + // Remove the path from manifest.json const removeFromManifest = async (targetPath) => { const manifest = await readJsonFile(MANIFEST_FILE) @@ -174,7 +246,6 @@ const addRedirect = async (deletedPath, redirectTo) => { console.log(`Redirect already exists for ${deletedPath}, updating destination to ${redirectTo}`) existingRedirect.destination = redirectTo } else { - console.log(`Adding new redirect: ${deletedPath} β†’ ${redirectTo}`) redirects.push({ source: deletedPath, destination: redirectTo, @@ -205,20 +276,24 @@ const deleteDoc = async (targetPath, redirectTo = '/docs/') => { throw new Error(`Target file does not exist: ${fullPath}`) } - console.log(`πŸ” Checking for references to ${targetPath}...`) + // console.log(`πŸ” Checking for references to ${targetPath}...`) - // Check if any files reference this path - const referencingFiles = await checkMdxReferences(targetPath) + // // Check if any files reference this path + // const referencingFiles = await checkMdxReferences(targetPath) - if (referencingFiles.length > 0) { - console.error(`❌ Cannot delete ${targetPath} - it is referenced by the following files:`) - referencingFiles.forEach((file) => console.error(` β€’ ${file}`)) - throw new Error(`File ${targetPath} is still referenced by ${referencingFiles.length} other files`) - } + // if (referencingFiles.length > 0) { + // console.error(`❌ Cannot delete ${targetPath} - it is referenced by the following files:`) + // referencingFiles.forEach((file) => console.error(` β€’ ${file}`)) + // throw new Error(`File ${targetPath} is still referenced by ${referencingFiles.length} other files`) + // } - console.log(`βœ… No references found to ${targetPath}`) + // console.log(`βœ… No references found to ${targetPath}`) try { + // Update links in MDX files + console.log(`πŸ”— Updating links in MDX files...`) + await updateMdxLinks(targetPath, redirectTo) + // Remove from manifest console.log(`πŸ“ Removing ${targetPath} from manifest.json...`) await removeFromManifest(targetPath) @@ -228,7 +303,7 @@ const deleteDoc = async (targetPath, redirectTo = '/docs/') => { await addRedirect(targetPath, redirectTo) // Delete the file - console.log(`πŸ—‘οΈ Deleting file: ${fullPath}`) + console.log(`πŸ—‘οΈ Deleting file: ${fullPath}`) await fs.unlink(fullPath) console.log(`βœ… Successfully deleted ${targetPath}`) From 3f0ec0e9693f9a94e692e8af6f5f6d1066e4b668 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:27:59 +0200 Subject: [PATCH 044/136] add use-migration-blank-page flag --- flags.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flags.json b/flags.json index 3703f53b7c..8b971d6dfe 100644 --- a/flags.json +++ b/flags.json @@ -1,4 +1,5 @@ { "use-proposal-manifest": true, - "top-level-manifest-splitting": true + "top-level-manifest-splitting": true, + "use-migration-blank-page": false } From 1bda73f54641dfe27a867c4cf20189d1c373d5a6 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 8 Aug 2025 23:18:34 +0800 Subject: [PATCH 045/136] Add support for arbitrary json in proposal.md --- proposal.md | 80 +++---- .../migration-assistant/generate-manifest.ts | 202 ++++++++---------- 2 files changed, 134 insertions(+), 148 deletions(-) diff --git a/proposal.md b/proposal.md index 5e0c35f9d2..55cd472bde 100644 --- a/proposal.md +++ b/proposal.md @@ -1,9 +1,9 @@ # Top Level Links -- Getting Started (checkmark-circle) -- Guides (book) -- Examples (plus-circle) -- Reference (globe) +- Getting Started {"icon": "checkmark-circle"} +- Guides {"icon": "book"} +- Examples {"icon": "plus-circle"} +- Reference {"icon": "globe"} - SDK Reference - API Reference - UI Components @@ -86,7 +86,7 @@ - OAuth - What are OAuth & OIDC - How Clerk implements OAuth - - Use OAuth for Single Sign-On (SSO) + - Use OAuth for Single Sign-On {"icon": "SSO"} - Use OAuth for scoped access - Verify OAuth tokens - Session Token customization @@ -121,7 +121,7 @@ ### Customizing Clerk -- UI Customization (Appearance Prop) [appearance-prop] +- UI Customization [appearance-prop] {"icon": "Appearance Prop"} - Overview - Layout - Themes @@ -133,10 +133,10 @@ - User profile - User button - Email and SMS Templates -- Localization (i18n) [localization] -- Clerk Elements (beta) [elements] +- Localization [localization] {"icon": "i18n"} +- Clerk Elements [elements] {"tag": "(Beta)"} -### B2B (Organizations) [organizations] +### B2B [organizations] {"icon": "Organizations"} - Overview - Managing Organizations [managing-orgs] @@ -155,12 +155,12 @@ ### Securing your App [secure] - Restricting Access -- Multifactor Authentication (MFA) [mfa] +- Multifactor Authentication [mfa] {"icon": "MFA"} - Authorization Checks - Bot Protection - Banning Users - Prevent brute force attacks [user-lockout] -- Reverification (Step-up) +- Reverification {"icon": "Step-up"} - Legal Compliance - Password protection and rules - Security Best Practices [best-practices] @@ -257,40 +257,40 @@ ### General [reference] -- UI Components (box) [components] +- UI Components [components] {"icon": "box"} - Hooks ### SDK Reference -- Next.js (nextjs) -- React (react) -- Expo (expo) -- JavaScript (javascript) -- Express (express) -- React Router (react-router) -- Astro (astro) -- iOS (ios) -- Nuxt (nuxt) -- Vue (vue) -- Chrome Extension (chrome) -- Fastify (fastify) -- Remix (remix) -- Tanstack Start (tanstack) -- JS Backend SDK (clerk) -- C# (c-sharp) -- Go (go) -- Java (java) -- Python (python) -- Ruby / Rails (ruby) +- Next.js {"icon": "nextjs"} +- React {"icon": "react"} +- Expo {"icon": "expo"} +- JavaScript {"icon": "javascript"} +- Express {"icon": "expressjs"} +- React Router {"icon": "react-router"} +- Astro {"icon": "astro"} +- iOS {"icon": "apple"} +- Nuxt {"icon": "nuxt"} +- Vue {"icon": "vue"} +- Chrome Extension {"icon": "chrome"} +- Fastify {"icon": "fastify"} +- Remix {"icon": "remix"} +- Tanstack Start {"icon": "tanstack"} +- JS Backend SDK {"icon": "clerk"} +- C# {"icon": "c-sharp"} +- Go {"icon": "go"} +- Java {"icon": "java"} +- Python {"icon": "python"} +- Ruby / Rails {"icon": "ruby"} - Community SDKs - - Angular (angular) - - Elysia (elysia) - - Hono (hono) - - Koa (koa) - - SolidJS (solid) - - Svelte (svelte) - - RedwoodJS (redwood) - - Rust (rust) + - Angular {"icon": "angular"} + - Elysia {"icon": "elysia"} + - Hono {"icon": "hono"} + - Koa {"icon": "koa"} + - SolidJS {"icon": "solid"} + - Svelte {"icon": "svelte"} + - RedwoodJS {"icon": "redwood"} + - Rust {"icon": "rust"} ### HTTP API Reference diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index b277d8ea85..58b5014928 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -5,103 +5,65 @@ import { z } from 'zod' const PROPOSAL_PATH = path.join(process.cwd(), './proposal.md') const OUTPUT_PATH = path.join(process.cwd(), './public/manifest.proposal.json') -// Valid icon names from the schema -const VALID_ICONS = [ - 'apple', - 'application-2', - 'arrow-up-circle', - 'astro', - 'angular', - 'block', - 'bolt', - 'book', - 'box', - 'c-sharp', - 'chart', - 'checkmark-circle', - 'chrome', - 'clerk', - 'code-bracket', - 'cog-6-teeth', - 'door', - 'elysia', - 'expressjs', - 'globe', - 'go', - 'home', - 'hono', - 'javascript', - 'koa', - 'link', - 'linkedin', - 'lock', - 'nextjs', - 'nodejs', - 'plug', - 'plus-circle', - 'python', - 'react', - 'redwood', - 'remix', - 'react-router', - 'rocket', - 'route', - 'ruby', - 'rust', - 'speedometer', - 'stacked-rectangle', - 'solid', - 'svelte', - 'tanstack', - 'user-circle', - 'user-dotted-circle', - 'vue', - 'x', - 'expo', - 'nuxt', - 'fastify', -] as const - -type ValidIcon = (typeof VALID_ICONS)[number] - /** - * Parse title, icon, and custom slug from text + * Parse title and custom slug from text * Supports formats like: - * - "Title (icon) [slug]" - * - "Title (icon)" * - "Title [slug]" * - "Title" + * Parentheses are treated as part of the title. */ -function parseTextWithIconAndSlug(text: string): { +function parseTextWithSlug(text: string): { title: string - icon: ValidIcon | null customSlug: string | null } { - // Match pattern: Title (icon) [slug] or any combination - const match = text.match(/^(.+?)(?:\s*\(([^)]+)\))?(?:\s*\[([^\]]+)\])?$/) + // Match pattern: Title [slug] + const match = text.match(/^(.+?)(?:\s*\[([^\]]+)\])?$/) if (!match) { - return { title: text.trim(), icon: null, customSlug: null } + return { title: text.trim(), customSlug: null } } - const [, titlePart, iconPart, slugPart] = match + const [, titlePart, slugPart] = match const title = titlePart.trim() - const icon = iconPart?.trim() const customSlug = slugPart?.trim() - // Validate icon if provided - const validIcon = icon && VALID_ICONS.includes(icon as ValidIcon) ? (icon as ValidIcon) : null - if (icon && !validIcon) { - console.warn(`Warning: Invalid icon "${icon}" for "${title}". Valid icons: ${VALID_ICONS.join(', ')}`) - } - return { title, - icon: validIcon, customSlug: customSlug || null, } } +/** + * Extract a trailing JSON object from the end of a line of text. + * Example: "My Title {\"abc\":\"xyz\"}" -> { baseText: "My Title", extraProps: { abc: "xyz" } } + */ +function extractTrailingJson(text: string): { baseText: string; extraProps: Record | null } { + const trimmedText = text.trim() + // Quick exit if it doesn't end with a closing brace + if (!trimmedText.endsWith('}')) { + return { baseText: text, extraProps: null } + } + + // Find the last opening brace and try to parse from there + const lastOpenIndex = trimmedText.lastIndexOf('{') + if (lastOpenIndex === -1) { + return { baseText: text, extraProps: null } + } + + const possibleJson = trimmedText.slice(lastOpenIndex) + try { + const parsed = JSON.parse(possibleJson) + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const baseText = trimmedText.slice(0, lastOpenIndex).trim() + return { baseText, extraProps: parsed as Record } + } + return { baseText: text, extraProps: null } + } catch { + // Not valid JSON – ignore silently and fall back to original text + return { baseText: text, extraProps: null } + } +} + // Convert to lowercase and replace spaces with hyphens const slugify = (str: string) => str @@ -173,12 +135,22 @@ function parseMarkdownToManifest(content: string) { // Process Top Level Links items if (inTopLevelLinksSection && trimmed.startsWith('- ')) { - const titleWithIconAndSlug = trimmed.substring(2) - const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) + const rawTopLinkText = trimmed.substring(2) + const { baseText: titleWithIconAndSlug, extraProps } = extractTrailingJson(rawTopLinkText) + const { title, customSlug } = parseTextWithSlug(titleWithIconAndSlug) + + // Determine icon: prefer JSON's icon + let finalIcon: string | undefined + const jsonIcon = (extraProps && typeof extraProps.icon === 'string' ? (extraProps.icon as string) : undefined) as + | string + | undefined + if (jsonIcon) { + finalIcon = jsonIcon + } // Store top-level link info for later merging with sections topLevelLinksMap.set(title, { - icon: icon || undefined, + icon: finalIcon || undefined, customSlug: customSlug || undefined, }) continue @@ -187,13 +159,21 @@ function parseMarkdownToManifest(content: string) { // Top level sections (## Header) - these become top-level groups if (trimmed.startsWith('## ')) { finishCurrentTopLevelGroup() - const titleWithIconAndSlug = trimmed.substring(3) - const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) + const rawHeaderText = trimmed.substring(3) + const { baseText: titleWithIconAndSlug, extraProps } = extractTrailingJson(rawHeaderText) + const { title, customSlug } = parseTextWithSlug(titleWithIconAndSlug) currentTopLevelGroup = { title, items: [], } + + // If icon provided via JSON, set it + if (extraProps && typeof extraProps.icon === 'string') { + const jsonIcon = extraProps.icon as string + ;(currentTopLevelGroup as any).icon = jsonIcon + } + currentTopLevelCustomSlug = customSlug currentSubGroup = null currentItemGroup = [] @@ -204,13 +184,21 @@ function parseMarkdownToManifest(content: string) { // Subsections (### Header) - these become sub-groups within top-level groups if (trimmed.startsWith('### ')) { finishCurrentSubGroup() - const titleWithIconAndSlug = trimmed.substring(4) - const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) + const rawHeaderText = trimmed.substring(4) + const { baseText: titleWithIconAndSlug, extraProps } = extractTrailingJson(rawHeaderText) + const { title, customSlug } = parseTextWithSlug(titleWithIconAndSlug) currentSubGroup = { title, items: [], } + + // If icon provided via JSON, set it + if (extraProps && typeof extraProps.icon === 'string') { + const jsonIcon = extraProps.icon as string + ;(currentSubGroup as any).icon = jsonIcon + } + currentSubGroupCustomSlug = customSlug currentItemGroup = [] pathStack = [] @@ -223,9 +211,11 @@ function parseMarkdownToManifest(content: string) { const leadingSpaces = line.length - line.trimStart().length const indentLevel = Math.floor(leadingSpaces / 2) // Assuming 2 spaces per indent level - const titleWithIconAndSlug = trimmed.substring(2) - // Parse icon and slug syntax for list items - const { title, icon, customSlug } = parseTextWithIconAndSlug(titleWithIconAndSlug) + const rawItemText = trimmed.substring(2) + // Support optional trailing JSON blob that should be merged into the item + const { baseText: titleWithIconAndSlug, extraProps } = extractTrailingJson(rawItemText) + // Parse slug syntax for list items + const { title, customSlug } = parseTextWithSlug(titleWithIconAndSlug) // Update pathStack based on indentation level // Keep only the path components for levels above the current one @@ -281,7 +271,6 @@ function parseMarkdownToManifest(content: string) { title, currentTopLevelGroup?.title, currentSubGroup?.title, - undefined, itemSlug, currentTopLevelCustomSlug || undefined, currentSubGroupCustomSlug || undefined, @@ -289,9 +278,17 @@ function parseMarkdownToManifest(content: string) { ), } - // Add icon if provided - if (icon) { - newItem.icon = icon + // Icon can only come from trailing JSON + + // Merge any extra properties parsed from the trailing JSON + if (extraProps) { + // If icon provided via JSON, set it + if (typeof extraProps.icon === 'string') { + const jsonIcon = extraProps.icon as string + newItem.icon = jsonIcon + } + + Object.assign(newItem, extraProps) } // Find the appropriate container for this indentation level @@ -312,12 +309,8 @@ function parseMarkdownToManifest(content: string) { const group = navGroup[0] if (group && group.title) { const topLevelInfo = topLevelLinksMap.get(group.title) - if (topLevelInfo) { - // Add icon from top-level links if available - if (topLevelInfo.icon) { - group.icon = topLevelInfo.icon - } - // Note: custom slug from top-level links could override section slug if needed + if (topLevelInfo && topLevelInfo.icon) { + group.icon = topLevelInfo.icon } } return navGroup @@ -333,7 +326,6 @@ function parseMarkdownToManifest(content: string) { * @param {string} title - The item title * @param {string} topLevel - The top-level group title * @param {string} subLevel - The sub-level group title - * @param {string} manifestKey - The manifest key for separate manifests * @param {string} customSlug - The custom slug for the item * @param {string} topLevelCustomSlug - The custom slug for the top-level group * @param {string} subLevelCustomSlug - The custom slug for the sub-level group @@ -344,7 +336,6 @@ function generateHref( title: string, _topLevel?: string, subLevel?: string, - manifestKey?: string, customSlug?: string, topLevelCustomSlug?: string, subLevelCustomSlug?: string, @@ -358,11 +349,6 @@ function generateHref( topLevel = '' } - // If this is a separate manifest, use the manifest key as the base path - if (manifestKey) { - href += `/${manifestKey}` - } - // Add the top-level group path if (topLevel) { const topLevelSlug = topLevelCustomSlug || slugify(topLevel) @@ -392,14 +378,14 @@ function generateHref( export type ManifestItem = { title: string href: string - icon?: ValidIcon + icon?: string } export type ManifestGroup = { title: string items: Manifest collapse?: boolean - icon?: ValidIcon + icon?: string } export type Manifest = (ManifestItem | ManifestGroup)[][] @@ -408,16 +394,16 @@ const manifestItem: z.ZodType = z .object({ title: z.string(), href: z.string(), - icon: z.enum(VALID_ICONS).optional(), + icon: z.string().optional(), }) - .strict() + .passthrough() const manifestGroup: z.ZodType = z .object({ title: z.string(), items: z.lazy(() => manifestSchema), collapse: z.boolean().optional(), - icon: z.enum(VALID_ICONS).optional(), + icon: z.string().optional(), }) .strict() From 500b336bb60a5b8cacd127ee834e8c1f0cf846fc Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 8 Aug 2025 23:23:10 +0800 Subject: [PATCH 046/136] Add {"collapse": false} to overright default collapse true --- scripts/migration-assistant/generate-manifest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index 58b5014928..2c962c6ab8 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -252,7 +252,10 @@ function parseMarkdownToManifest(content: string) { // Convert the parent item to a group if it's not already one if (!parentItem.items) { parentItem.items = [[]] - parentItem.collapse = true + // Only set default collapse if not explicitly set via JSON + if (typeof parentItem.collapse === 'undefined') { + parentItem.collapse = true + } delete parentItem.href // Groups don't have hrefs } From 87c788b3726e317bd9abad0ef456dd0fa2135264 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 8 Aug 2025 23:30:57 +0800 Subject: [PATCH 047/136] fix up headings --- proposal.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposal.md b/proposal.md index 55cd472bde..ed13c18a59 100644 --- a/proposal.md +++ b/proposal.md @@ -86,7 +86,7 @@ - OAuth - What are OAuth & OIDC - How Clerk implements OAuth - - Use OAuth for Single Sign-On {"icon": "SSO"} + - Use OAuth for Single Sign-On (SSO) - Use OAuth for scoped access - Verify OAuth tokens - Session Token customization @@ -121,7 +121,7 @@ ### Customizing Clerk -- UI Customization [appearance-prop] {"icon": "Appearance Prop"} +- UI Customization (Appearance Prop) [appearance-prop] - Overview - Layout - Themes @@ -133,10 +133,10 @@ - User profile - User button - Email and SMS Templates -- Localization [localization] {"icon": "i18n"} +- Localization (i18n) [localization] - Clerk Elements [elements] {"tag": "(Beta)"} -### B2B [organizations] {"icon": "Organizations"} +### B2B (Organizations) [organizations] - Overview - Managing Organizations [managing-orgs] @@ -155,12 +155,12 @@ ### Securing your App [secure] - Restricting Access -- Multifactor Authentication [mfa] {"icon": "MFA"} +- Multifactor Authentication (MFA) [mfa] - Authorization Checks - Bot Protection - Banning Users - Prevent brute force attacks [user-lockout] -- Reverification {"icon": "Step-up"} +- Reverification (Step-up) - Legal Compliance - Password protection and rules - Security Best Practices [best-practices] From 9bbb93fc5af63612da452dd1314dd79f3e126ebe Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Sat, 9 Aug 2025 10:23:32 +0300 Subject: [PATCH 048/136] add tag as an expected type --- scripts/migration-assistant/generate-manifest.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/migration-assistant/generate-manifest.ts b/scripts/migration-assistant/generate-manifest.ts index 2c962c6ab8..e8977448d6 100755 --- a/scripts/migration-assistant/generate-manifest.ts +++ b/scripts/migration-assistant/generate-manifest.ts @@ -382,6 +382,7 @@ export type ManifestItem = { title: string href: string icon?: string + tag?: string } export type ManifestGroup = { @@ -389,6 +390,7 @@ export type ManifestGroup = { items: Manifest collapse?: boolean icon?: string + tag?: string } export type Manifest = (ManifestItem | ManifestGroup)[][] @@ -398,6 +400,7 @@ const manifestItem: z.ZodType = z title: z.string(), href: z.string(), icon: z.string().optional(), + tag: z.string().optional(), }) .passthrough() @@ -407,6 +410,7 @@ const manifestGroup: z.ZodType = z items: z.lazy(() => manifestSchema), collapse: z.boolean().optional(), icon: z.string().optional(), + tag: z.string().optional(), }) .strict() From c103b3867c975ed008731bd79c0f336e1a154701 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:16:29 +0300 Subject: [PATCH 049/136] IA: Configuring your app (/configure) section (#2453) Co-authored-by: Nick Wylynko --- docs/_partials/user-object.mdx | 4 +- docs/account-portal/overview.mdx | 4 +- .../configuration/email-sms-templates.mdx | 4 +- .../configuration/force-mfa.mdx | 2 +- .../configuration/restrictions.mdx | 4 +- .../configuration/session-options.mdx | 2 +- docs/authentication/overview.mdx | 30 - docs/backend-requests/jwt-templates.mdx | 4 +- .../resources/session-tokens.mdx | 12 +- .../authentication/google-one-tap.mdx | 2 +- docs/components/authentication/sign-in.mdx | 2 +- docs/components/authentication/sign-up.mdx | 2 +- .../control/authenticate-with-callback.mdx | 2 +- docs/custom-flows/email-links.mdx | 2 +- docs/custom-flows/email-password-mfa.mdx | 2 +- docs/custom-flows/enterprise-connections.mdx | 2 +- docs/custom-flows/forgot-password.mdx | 2 +- docs/custom-flows/google-one-tap.mdx | 2 +- docs/custom-flows/manage-sms-based-mfa.mdx | 2 +- docs/custom-flows/manage-sso-connections.mdx | 6 +- docs/custom-flows/manage-totp-based-mfa.mdx | 2 +- .../multi-session-applications.mdx | 2 +- docs/custom-flows/oauth-connections.mdx | 2 +- docs/custom-flows/overview.mdx | 4 +- docs/custom-flows/passkeys.mdx | 2 +- .../elements/reference/common.mdx | 2 +- docs/deployments/overview.mdx | 8 +- .../account-linking.mdx | 0 .../authentication-flows.mdx | 4 +- .../enterprise-connections/easie/google.mdx | 6 +- .../easie/microsoft.mdx | 6 +- .../jit-provisioning.mdx | 2 +- .../oidc/custom-provider.mdx | 2 +- .../enterprise-connections/overview.mdx | 14 +- .../enterprise-connections/saml/azure.mdx | 2 +- .../saml/custom-provider.mdx | 4 +- .../enterprise-connections/saml/google.mdx | 2 +- .../enterprise-connections/saml/okta.mdx | 2 +- .../oauth/how-clerk-implements-oauth.mdx | 0 .../auth-strategies}/oauth/overview.mdx | 8 +- .../auth-strategies}/oauth/scoped-access.mdx | 10 +- .../auth-strategies}/oauth/single-sign-on.mdx | 8 +- .../oauth/verify-oauth-tokens.mdx | 2 +- .../sign-up-sign-in-options.mdx | 6 +- .../social-connections/account-linking.mdx | 0 .../social-connections/apple.mdx | 0 .../social-connections/atlassian.mdx | 0 .../social-connections/bitbucket.mdx | 0 .../social-connections/box.mdx | 0 .../social-connections/coinbase.mdx | 0 .../social-connections/custom-provider.mdx | 0 .../social-connections/discord.mdx | 0 .../social-connections/dropbox.mdx | 0 .../social-connections/facebook.mdx | 0 .../social-connections/github.mdx | 0 .../social-connections/gitlab.mdx | 0 .../social-connections/google.mdx | 0 .../social-connections/hubspot.mdx | 0 .../social-connections/hugging-face.mdx} | 0 .../social-connections/line.mdx | 0 .../social-connections/linear.mdx | 0 .../social-connections/linkedin-oidc.mdx | 0 .../social-connections/linkedin.mdx | 4 +- .../social-connections/microsoft.mdx | 4 +- .../social-connections/notion.mdx | 0 .../social-connections/oauth.mdx | 52 +- .../social-connections/overview.mdx | 6 +- .../social-connections/slack.mdx | 0 .../social-connections/spotify.mdx | 0 .../social-connections/tiktok.mdx | 0 .../social-connections/twitch.mdx | 0 .../social-connections/twitter.mdx | 4 +- .../social-connections/x-twitter.mdx | 0 .../social-connections/xero.mdx | 0 .../auth-strategies}/web3/coinbase-wallet.mdx | 2 +- .../auth-strategies}/web3/metamask.mdx | 0 .../auth-strategies}/web3/okx-wallet.mdx | 0 .../analytics/google-analytics.mdx | 0 .../integrations/databases/convex.mdx | 0 .../integrations/databases/fauna.mdx | 0 .../integrations/databases/firebase.mdx | 2 +- .../integrations/databases/grafbase.mdx | 0 .../integrations/databases/hasura.mdx | 0 .../integrations/databases/instantdb.mdx | 0 .../integrations/databases/neon.mdx | 0 .../integrations/databases/nhost.mdx | 0 .../integrations/databases/supabase.mdx | 2 +- .../configure}/integrations/overview.mdx | 26 +- .../integrations/platforms}/shopify.mdx | 0 .../platforms}/vercel-marketplace.mdx | 2 +- .../configure/session-token.mdx} | 0 .../configure/webhooks/debugging.mdx} | 8 +- .../configure}/webhooks/inngest.mdx | 8 +- .../{ => guides/configure}/webhooks/loops.mdx | 0 .../configure}/webhooks/overview.mdx | 2 +- .../configure/webhooks/syncing.mdx} | 6 +- docs/guides/force-token-refresh.mdx | 4 +- docs/hooks/use-reverification.mdx | 2 +- docs/index.mdx | 6 +- docs/manifest.json | 142 +++-- docs/organizations/invitations.mdx | 2 +- docs/organizations/manage-sso.mdx | 4 +- docs/organizations/org-slugs-in-urls.mdx | 2 +- docs/quickstarts/setup-clerk.mdx | 4 +- .../phone-numbers/create-phone-number.mdx | 2 +- .../phone-numbers/update-phone-number.mdx | 2 +- .../user/get-user-oauth-access-token.mdx | 2 +- docs/references/backend/verify-webhook.mdx | 2 +- docs/references/expo/passkeys.mdx | 2 +- .../references/expo/use-local-credentials.mdx | 2 +- docs/references/expo/use-oauth.mdx | 2 +- docs/references/expo/use-sso.mdx | 6 +- docs/references/ios/auth-view.mdx | 2 +- docs/references/ios/sign-in-with-apple.mdx | 2 +- docs/references/javascript/clerk.mdx | 2 +- docs/references/javascript/sign-in.mdx | 42 +- docs/references/javascript/sign-up.mdx | 50 +- .../javascript/types/email-address.mdx | 2 +- .../javascript/types/phone-number.mdx | 2 +- docs/references/javascript/types/sso.mdx | 4 +- docs/references/javascript/user.mdx | 12 +- docs/references/nextjs/authjs-migration.mdx | 4 +- docs/testing/test-emails-and-phones.mdx | 2 +- docs/troubleshooting/email-deliverability.mdx | 2 +- docs/users/metadata.mdx | 4 +- proposal-mapping.json | 2 +- proposal.md | 73 +-- redirects/static/docs.json | 532 +++++++++++++++--- scripts/build-docs.ts | 8 + 129 files changed, 789 insertions(+), 462 deletions(-) delete mode 100644 docs/authentication/overview.mdx rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/account-linking.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/authentication-flows.mdx (91%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/easie/google.mdx (88%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/easie/microsoft.mdx (83%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/jit-provisioning.mdx (94%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/oidc/custom-provider.mdx (98%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/overview.mdx (83%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/saml/azure.mdx (98%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/saml/custom-provider.mdx (93%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/saml/google.mdx (98%) rename docs/{authentication => guides/configure/auth-strategies}/enterprise-connections/saml/okta.mdx (98%) rename docs/{ => guides/configure/auth-strategies}/oauth/how-clerk-implements-oauth.mdx (100%) rename docs/{ => guides/configure/auth-strategies}/oauth/overview.mdx (95%) rename docs/{ => guides/configure/auth-strategies}/oauth/scoped-access.mdx (91%) rename docs/{ => guides/configure/auth-strategies}/oauth/single-sign-on.mdx (96%) rename docs/{ => guides/configure/auth-strategies}/oauth/verify-oauth-tokens.mdx (85%) rename docs/{authentication/configuration => guides/configure/auth-strategies}/sign-up-sign-in-options.mdx (90%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/account-linking.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/apple.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/atlassian.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/bitbucket.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/box.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/coinbase.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/custom-provider.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/discord.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/dropbox.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/facebook.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/github.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/gitlab.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/google.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/hubspot.mdx (100%) rename docs/{authentication/social-connections/huggingface.mdx => guides/configure/auth-strategies/social-connections/hugging-face.mdx} (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/line.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/linear.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/linkedin-oidc.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/linkedin.mdx (94%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/microsoft.mdx (89%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/notion.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/oauth.mdx (68%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/overview.mdx (97%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/slack.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/spotify.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/tiktok.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/twitch.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/twitter.mdx (95%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/x-twitter.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/social-connections/xero.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/web3/coinbase-wallet.mdx (97%) rename docs/{authentication => guides/configure/auth-strategies}/web3/metamask.mdx (100%) rename docs/{authentication => guides/configure/auth-strategies}/web3/okx-wallet.mdx (100%) rename docs/{ => guides/configure}/integrations/analytics/google-analytics.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/convex.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/fauna.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/firebase.mdx (99%) rename docs/{ => guides/configure}/integrations/databases/grafbase.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/hasura.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/instantdb.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/neon.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/nhost.mdx (100%) rename docs/{ => guides/configure}/integrations/databases/supabase.mdx (99%) rename docs/{ => guides/configure}/integrations/overview.mdx (94%) rename docs/{integrations => guides/configure/integrations/platforms}/shopify.mdx (100%) rename docs/{integrations => guides/configure/integrations/platforms}/vercel-marketplace.mdx (96%) rename docs/{backend-requests/custom-session-token.mdx => guides/configure/session-token.mdx} (100%) rename docs/{webhooks/debug-your-webhooks.mdx => guides/configure/webhooks/debugging.mdx} (89%) rename docs/{ => guides/configure}/webhooks/inngest.mdx (88%) rename docs/{ => guides/configure}/webhooks/loops.mdx (100%) rename docs/{ => guides/configure}/webhooks/overview.mdx (99%) rename docs/{webhooks/sync-data.mdx => guides/configure/webhooks/syncing.mdx} (97%) diff --git a/docs/_partials/user-object.mdx b/docs/_partials/user-object.mdx index 92a9d814ed..614ad82731 100644 --- a/docs/_partials/user-object.mdx +++ b/docs/_partials/user-object.mdx @@ -1,5 +1,5 @@ -The `User` object holds all of the information for a single user of your application and provides a set of methods to manage their account. Each `User` has at least one authentication [identifier](/docs/authentication/configuration/sign-up-sign-in-options#identifiers), which might be their email address, phone number, or a username. +The `User` object holds all of the information for a single user of your application and provides a set of methods to manage their account. Each `User` has at least one authentication [identifier](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#identifiers), which might be their email address, phone number, or a username. -A user can be contacted at their primary email address or primary phone number. They can have more than one registered email address, but only one of them will be their primary email address. This goes for phone numbers as well; a user can have more than one, but only one phone number will be their primary. At the same time, a user can also have one or more external accounts by connecting to [social providers](/docs/authentication/social-connections/oauth) such as Google, Apple, Facebook, and many more. +A user can be contacted at their primary email address or primary phone number. They can have more than one registered email address, but only one of them will be their primary email address. This goes for phone numbers as well; a user can have more than one, but only one phone number will be their primary. At the same time, a user can also have one or more external accounts by connecting to [social providers](/docs/guides/configure/auth-strategies/social-connections/oauth) such as Google, Apple, Facebook, and many more. Finally, a `User` object holds profile data like the user's name, profile picture, and a set of [metadata](/docs/users/metadata) that can be used internally to store arbitrary information. The metadata are split into `publicMetadata` and `privateMetadata`. Both types are set from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }}, but public metadata can also be accessed from the [Frontend API](/docs/reference/frontend-api){{ target: '_blank' }}. diff --git a/docs/account-portal/overview.mdx b/docs/account-portal/overview.mdx index d9d897e353..82ff56084d 100644 --- a/docs/account-portal/overview.mdx +++ b/docs/account-portal/overview.mdx @@ -30,7 +30,7 @@ For each application environment, Clerk provides pages for sign-up, sign-in, use ### Sign-in -The sign-in page hosts the prebuilt [``](/docs/components/authentication/sign-in) component, which renders a UI for signing in users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-up and sign-in options](/docs/authentication/configuration/sign-up-sign-in-options) and [social connections](/docs/authentication/social-connections/oauth). +The sign-in page hosts the prebuilt [``](/docs/components/authentication/sign-in) component, which renders a UI for signing in users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-up and sign-in options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/oauth). ![The Account Portal sign-in page hosts the \ component](/docs/images/account-portal/sign-in.png) @@ -38,7 +38,7 @@ Redirect users to the sign-in page using the [``](/docs/comp ### Sign-up -The sign-up page hosts the prebuilt [``](/docs/components/authentication/sign-up) component, which renders a UI for signing up users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-up and sign-in options](/docs/authentication/configuration/sign-up-sign-in-options) and [social connections](/docs/authentication/social-connections/oauth). +The sign-up page hosts the prebuilt [``](/docs/components/authentication/sign-up) component, which renders a UI for signing up users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-up and sign-in options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/oauth). ![The Account Portal sign-up page hosts the \ component](/docs/images/account-portal/sign-up.png) diff --git a/docs/authentication/configuration/email-sms-templates.mdx b/docs/authentication/configuration/email-sms-templates.mdx index b0b253bd73..183656b986 100644 --- a/docs/authentication/configuration/email-sms-templates.mdx +++ b/docs/authentication/configuration/email-sms-templates.mdx @@ -59,7 +59,7 @@ The following operations are available for an email template: The following settings can be changed per email template: -- **Delivered by Clerk**: Clerk will deliver your emails using its own Email Service Provider (ESP), which is currently [SendGrid](https://sendgrid.com/). However, if you wish to handle delivery of emails on your own, then you can toggle this setting off. This means that Clerk will no longer be sending this particular email and in order to deliver it yourself, you will need to listen to the `emails.created` [webhook](/docs/webhooks/overview) and extract the necessary info from the event payload. +- **Delivered by Clerk**: Clerk will deliver your emails using its own Email Service Provider (ESP), which is currently [SendGrid](https://sendgrid.com/). However, if you wish to handle delivery of emails on your own, then you can toggle this setting off. This means that Clerk will no longer be sending this particular email and in order to deliver it yourself, you will need to listen to the `emails.created` [webhook](/docs/guides/configure/webhooks/overview) and extract the necessary info from the event payload. - **Name**: a name for the template on the template listing page. It does not affect the outgoing email in any way. - **From**: the email address that will be used as the **From** address in the email header. The format is `@`. You can change the local part to a custom value. If no value is provided, it defaults `noreply`. - **Reply-To**: the email address that will be used as the **Reply-To** address in the email header. This is useful if you want to direct replies to a different email address than the one used in the **From** field. The format is `@`. You can change the local part to a custom value. If no value is provided, the **Reply-To** header will be omitted. @@ -111,7 +111,7 @@ To access the SMS templates: Clerk will deliver your SMS messages using its own SMS Gateway. However, if you wish to handle SMS delivery on your own, then you can toggle **Delivered by Clerk** off. -This means that Clerk will no longer be sending this particular SMS and in order to deliver it yourself, you will need to listen to the `sms.created` [webhook](/docs/webhooks/overview) and extract the necessary info from the event payload. +This means that Clerk will no longer be sending this particular SMS and in order to deliver it yourself, you will need to listen to the `sms.created` [webhook](/docs/guides/configure/webhooks/overview) and extract the necessary info from the event payload. > [!NOTE] > Remember, this is a per-template setting and will not be applied to other templates. diff --git a/docs/authentication/configuration/force-mfa.mdx b/docs/authentication/configuration/force-mfa.mdx index 0d26eb47df..2bff7fc7fe 100644 --- a/docs/authentication/configuration/force-mfa.mdx +++ b/docs/authentication/configuration/force-mfa.mdx @@ -3,7 +3,7 @@ title: Force multi-factor authentication (MFA) for all users description: Learn how to force multi-factor authentication (MFA) for all users in your Clerk application. --- -By default, Clerk does not enforce [multi-factor authentication (MFA)](/docs/authentication/configuration/sign-up-sign-in-options#multi-factor-authentication) for all users. This guide demonstrates how to force MFA for all users by using `clerkMiddleware()` to intercept all requests and check whether a user has MFA enabled. If the user does not have MFA enabled, `clerkMiddleware()` redirects them to the `/mfa` page where they can set up MFA. +By default, Clerk does not enforce [multi-factor authentication (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#multi-factor-authentication) for all users. This guide demonstrates how to force MFA for all users by using `clerkMiddleware()` to intercept all requests and check whether a user has MFA enabled. If the user does not have MFA enabled, `clerkMiddleware()` redirects them to the `/mfa` page where they can set up MFA. ## Enable MFA in the Clerk Dashboard diff --git a/docs/authentication/configuration/restrictions.mdx b/docs/authentication/configuration/restrictions.mdx index 909a859bea..d7b33809a1 100644 --- a/docs/authentication/configuration/restrictions.mdx +++ b/docs/authentication/configuration/restrictions.mdx @@ -11,7 +11,7 @@ search: - waitlist --- -Clerk provides restriction options that give you enhanced control over who can access your application. These options enable you to limit sign-ups, sign-ins, or prevent accounts with specific [identifiers](/docs/authentication/configuration/sign-up-sign-in-options#identifiers), such as email addresses, phone numbers, and even entire domains, from accessing your application. +Clerk provides restriction options that give you enhanced control over who can access your application. These options enable you to limit sign-ups, sign-ins, or prevent accounts with specific [identifiers](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#identifiers), such as email addresses, phone numbers, and even entire domains, from accessing your application. ## Sign-up modes @@ -31,7 +31,7 @@ In **Public** mode, the sign-up process is open to anyone. This mode is ideal fo ### Restricted -In **Restricted** mode, user access is controlled by the application admin(s). Users can be added to the application through [invitations](/docs/users/invitations), [enterprise connections](/docs/authentication/enterprise-connections/overview) or [manual user creation](/docs/users/creating-users). This mode is ideal for applications that are in private beta or internal tools. +In **Restricted** mode, user access is controlled by the application admin(s). Users can be added to the application through [invitations](/docs/users/invitations), [enterprise connections](/docs/guides/configure/auth-strategies/enterprise-connections/overview) or [manual user creation](/docs/users/creating-users). This mode is ideal for applications that are in private beta or internal tools. Additional features available in **Restricted** mode: diff --git a/docs/authentication/configuration/session-options.mdx b/docs/authentication/configuration/session-options.mdx index b7c31fb35c..525f4a5caa 100644 --- a/docs/authentication/configuration/session-options.mdx +++ b/docs/authentication/configuration/session-options.mdx @@ -138,4 +138,4 @@ If a sign-in URL is set, either through the [`signInUrl`](/docs/components/clerk Session tokens are JWTs that contain a set of [default claims](/docs/backend-requests/resources/session-tokens) required by Clerk. You can customize these tokens by providing additional claims of your own. -To learn how to customize session tokens, see the [dedicated guide](/docs/backend-requests/custom-session-token). +To learn how to customize session tokens, see the [dedicated guide](/docs/guides/configure/session-token). diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx deleted file mode 100644 index 736d96cd6c..0000000000 --- a/docs/authentication/overview.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Sign-up & sign-in overview -description: Learn how to configure authentication and user management for your Clerk application. ---- - -Clerk supports multiple authentication strategies so that you can implement the strategy that makes sense for _your_ users. You can use the [Account Portal](/docs/account-portal/overview), [prebuilt components](/docs/components/overview), or build your own [custom flows](/docs/custom-flows/overview). - -## Configuration - -Configuring your application is done through the [Clerk Dashboard](https://dashboard.clerk.com). The Clerk Dashboard is where you, as the application owner, can manage your application's settings, users, and organizations. For example, if you want to enable phone number authentication, multi-factor authentication, social providers like Google, delete users, or create organizations, you can do all of this and more in the Clerk Dashboard. You can even invite other users to join your [organization workspace](/docs/organizations/organization-workspaces) and help configure and manage your application with you. - -## SSO authentication - -SSO authentication allows users to sign in to your application using an existing account from an external identity provider (IdP), such as Google. - -Clerk supports the following SSO types: - -- [OAuth SSO, also known as social connections or social providers](/docs/authentication/social-connections/oauth) -- [Enterprise SSO](/docs/authentication/enterprise-connections/overview) - - SAML - - OIDC - - EASIE - -## Web3 authentication - -Clerk supports the following Web3 providers: - -- [Coinbase Wallet](/docs/authentication/web3/coinbase-wallet) -- [Metamask](/docs/authentication/web3/metamask) -- [OKX Wallet](/docs/authentication/web3/okx-wallet) diff --git a/docs/backend-requests/jwt-templates.mdx b/docs/backend-requests/jwt-templates.mdx index c1cdf99ae3..f51141db33 100644 --- a/docs/backend-requests/jwt-templates.mdx +++ b/docs/backend-requests/jwt-templates.mdx @@ -4,7 +4,7 @@ description: Learn how to create custom JWT templates to generate JSON Web Token --- > [!WARNING] -> This guide is for creating custom JWT templates in order to generate JSON Web Tokens with Clerk. If you are looking for how to customize your Clerk-generated session token, refer to the [Customize your session token](/docs/backend-requests/custom-session-token) guide. +> This guide is for creating custom JWT templates in order to generate JSON Web Tokens with Clerk. If you are looking for how to customize your Clerk-generated session token, refer to the [Customize your session token](/docs/guides/configure/session-token) guide. Clerk offers the ability to generate [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWTs). Each JWT, or token, represents a user that is currently signed in to your application. @@ -70,7 +70,7 @@ For custom JWTs, Clerk automatically includes the following default claims: However, other default claims - such as `sid` (session ID) - are only included in session-bound tokens, not in custom JWTs. Custom JWTs are not inherently tied to a session. Instead, they're designed to be flexible tokens that include the default claims listed above, along with any additional claims you explicitly set in your JWT template. For this reason, session-tied claims like `sid`, `v`, `pla`, or `fea` cannot be included in custom JWTs. -If you need to generate a token that includes both session-bound data (like `sid`) and any additional claims, the recommended approach is to use a custom session token instead of a JWT template. See the [guide on customizing your Clerk session token](/docs/backend-requests/custom-session-token). +If you need to generate a token that includes both session-bound data (like `sid`) and any additional claims, the recommended approach is to use a custom session token instead of a JWT template. See the [guide on customizing your Clerk session token](/docs/guides/configure/session-token). ### Shortcodes diff --git a/docs/backend-requests/resources/session-tokens.mdx b/docs/backend-requests/resources/session-tokens.mdx index 2dee259037..76d38a2d16 100644 --- a/docs/backend-requests/resources/session-tokens.mdx +++ b/docs/backend-requests/resources/session-tokens.mdx @@ -90,9 +90,7 @@ Read more about Clerk session tokens and how they work in [the guide on how Cler -## Custom claims - -If you would like to add custom claims to your session token, you can [customize it](/docs/backend-requests/custom-session-token). +If you would like to add custom claims to your session token, you can [customize it](/docs/guides/configure/session-token). You can also create custom tokens using a [JWT template](/docs/backend-requests/jwt-templates). @@ -100,7 +98,7 @@ You can also create custom tokens using a [JWT template](/docs/backend-requests/ Clerk stores the session token in a cookie, and [**most browsers limit cookie size to 4KB**](https://datatracker.ietf.org/doc/html/rfc2109#section-6.3). Exceeding this limit will cause the cookie to not be set, which will break your app as Clerk depends on cookies to work properly. -A session token with the [default session claims](#default-claims) won't run into this issue, as this configuration produces a cookie significantly smaller than 4KB. However, you must consider this limit when implementing a [custom session token](/docs/backend-requests/custom-session-token). After accounting for the size of Clerk's default claims, the cookie can support **up to 1.2KB** of custom claims. +A session token with the [default session claims](#default-claims) won't run into this issue, as this configuration produces a cookie significantly smaller than 4kb. However, this limitation becomes relevant when implementing a [custom session token](/docs/guides/configure/session-token). In this case, it's recommended to move particularly large claims out of the token and fetch these using a separate API call from your backend. Claims to monitor for size limits: @@ -110,14 +108,14 @@ Claims to monitor for size limits: - `org.public_metadata` - `org_membership.public_metadata` -If you add any of these custom claims in your token, use caution to ensure the stored data doesn't exceed the size limit. It's highly recommended to [store the extra data in your own database](/docs/webhooks/sync-data#storing-extra-user-data) instead of storing it in metadata in the session token. If this isn't an option, you can [move particularly large claims like these out of the token](#example) and fetch them using a separate API call from your backend. +If you add any of these custom claims in your token, use caution to ensure the stored data doesn't exceed the size limit. It's highly recommended to [store the extra data in your own database](/docs/guides/configure/webhooks/syncing#storing-extra-user-data) instead of storing it in metadata in the session token. If this isn't an option, you can [move particularly large claims like these out of the token](#example) and fetch them using a separate API call from your backend. > [!NOTE] -> If your application encounters this issue, the Clerk Dashboard will display a warning: **"Some users are exceeding cookie size limits"**. To resolve this, update your [custom session token](/docs/backend-requests/custom-session-token). +> If your application encounters this issue, the Clerk Dashboard will display a warning: **"Some users are exceeding cookie size limits"**. To resolve this, update your [custom session token](/docs/guides/configure/session-token). ### Example -It's recommended to keep the total size of custom claims in the session token under 1.2KB. The following example shows how to move particularly large claims out of the session token and fetch them using a separate API call from your backend. The limitations of this approach are that if you make this call to Clerk's Backend API frequently, you risk hitting [rate limits](/docs/backend-requests/resources/rate-limits) and it's also slower than making a database query. We highly recommend [storing the extra data in your own database](/docs/webhooks/sync-data#storing-extra-user-data) instead of storing it in metadata in the session token. +It's recommended to keep the total size of custom claims in the session token under 1.2KB. The following example shows how to move particularly large claims out of the session token and fetch them using a separate API call from your backend. The limitations of this approach are that if you make this call to Clerk's Backend API frequently, you risk hitting [rate limits](/docs/backend-requests/resources/rate-limits) and it's also slower than making a database query. We highly recommend [storing the extra data in your own database](/docs/guides/configure/webhooks/syncing#storing-extra-user-data) instead of storing it in metadata in the session token. For example, if you were storing several fields in `user.public_metadata`, like this: diff --git a/docs/components/authentication/google-one-tap.mdx b/docs/components/authentication/google-one-tap.mdx index 2c348bf210..a8b302153b 100644 --- a/docs/components/authentication/google-one-tap.mdx +++ b/docs/components/authentication/google-one-tap.mdx @@ -5,7 +5,7 @@ sdk: astro, expo, nextjs, nuxt, react, react-router, remix, tanstack-react-start --- > [!IMPORTANT] -> To use Google One Tap with Clerk, you must [enable Google as a social connection in the Clerk Dashboard](/docs/authentication/social-connections/google#configure-for-your-production-instance) and make sure to use custom credentials. +> To use Google One Tap with Clerk, you must [enable Google as a social connection in the Clerk Dashboard](/docs/guides/configure/auth-strategies/social-connections/google#configure-for-your-production-instance) and make sure to use custom credentials. The `` component renders the [Google One Tap](https://developers.google.com/identity/gsi/web/guides/features) UI so that users can use a single button to sign-up or sign-in to your Clerk application with their Google accounts. diff --git a/docs/components/authentication/sign-in.mdx b/docs/components/authentication/sign-in.mdx index bf83de8201..f8bb94384b 100644 --- a/docs/components/authentication/sign-in.mdx +++ b/docs/components/authentication/sign-in.mdx @@ -6,7 +6,7 @@ sdk: astro, chrome-extension, expo, nextjs, nuxt, react, react-router, remix, ta ![The \ component renders a UI for signing in users.](/docs/images/ui-components/sign-in.png){{ style: { maxWidth: '460px' } }} -The `` component renders a UI to allow users to sign in or sign up by default. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/authentication/configuration/sign-up-sign-in-options) and [social connections](/docs/authentication/social-connections/oauth). You can further customize your `` component by passing additional [properties](#properties) at the time of rendering. +The `` component renders a UI to allow users to sign in or sign up by default. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/oauth). You can further customize your `` component by passing additional [properties](#properties) at the time of rendering. > [!NOTE] > The `` and `` components cannot render when a user is already signed in, unless the application allows multiple sessions. If a user is already signed in and the application only allows a single session, Clerk will redirect the user to the Home URL instead. diff --git a/docs/components/authentication/sign-up.mdx b/docs/components/authentication/sign-up.mdx index 3ebb697c08..5908d072a2 100644 --- a/docs/components/authentication/sign-up.mdx +++ b/docs/components/authentication/sign-up.mdx @@ -6,7 +6,7 @@ sdk: astro, chrome-extension, expo, nextjs, nuxt, react, react-router, remix, ta ![The \ component renders a UI for signing up users.](/docs/images/ui-components/sign-up.png){{ style: { maxWidth: '460px' } }} -The `` component renders a UI for signing up users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/authentication/configuration/sign-up-sign-in-options) and [social connections](/docs/authentication/social-connections/oauth). You can further customize your `` component by passing additional [properties](#properties) at the time of rendering. +The `` component renders a UI for signing up users. The functionality of the `` component is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/oauth). You can further customize your `` component by passing additional [properties](#properties) at the time of rendering. > [!NOTE] > The `` and `` components cannot render when a user is already signed in, unless the application allows multiple sessions. If a user is already signed in and the application only allows a single session, Clerk will redirect the user to the Home URL instead. diff --git a/docs/components/control/authenticate-with-callback.mdx b/docs/components/control/authenticate-with-callback.mdx index fc1c08a78c..5b708383f8 100644 --- a/docs/components/control/authenticate-with-callback.mdx +++ b/docs/components/control/authenticate-with-callback.mdx @@ -72,7 +72,7 @@ All props are optional. - `secondFactorUrl?` - `string | undefined` - The full URL or path to navigate to during sign in, if [multi-factor authentication](/docs/authentication/configuration/sign-up-sign-in-options#multi-factor-authentication) is enabled. + The full URL or path to navigate to during sign in, if [multi-factor authentication](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#multi-factor-authentication) is enabled. --- diff --git a/docs/custom-flows/email-links.mdx b/docs/custom-flows/email-links.mdx index 8bde3f7ee2..3aed78cdd5 100644 --- a/docs/custom-flows/email-links.mdx +++ b/docs/custom-flows/email-links.mdx @@ -10,7 +10,7 @@ description: Learn how to build a custom flow using Clerk's API to handle email > Expo does not support email links. You can request this feature on [Clerk's roadmap](https://feedback.clerk.com/). -[Email links](/docs/authentication/configuration/sign-up-sign-in-options) can be used to sign up new users, sign in existing users, or allow existing users to verify newly added email addresses to their user profiles. +[Email links](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) can be used to sign up new users, sign in existing users, or allow existing users to verify newly added email addresses to their user profiles. The email link flow works as follows: diff --git a/docs/custom-flows/email-password-mfa.mdx b/docs/custom-flows/email-password-mfa.mdx index f68259f191..87a8beb716 100644 --- a/docs/custom-flows/email-password-mfa.mdx +++ b/docs/custom-flows/email-password-mfa.mdx @@ -5,7 +5,7 @@ description: Learn how to build a custom email/password sign-in flow that requir -[Multi-factor verification (MFA)](/docs/authentication/configuration/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. +[Multi-factor verification (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. Clerk supports second factor verification through **SMS verification code**, **Authenticator application**, and **Backup codes**. diff --git a/docs/custom-flows/enterprise-connections.mdx b/docs/custom-flows/enterprise-connections.mdx index 2e8391bc03..0f991313f1 100644 --- a/docs/custom-flows/enterprise-connections.mdx +++ b/docs/custom-flows/enterprise-connections.mdx @@ -7,7 +7,7 @@ description: Learn how to use the Clerk API to build a custom sign-up and sign-i ## Before you start -You must configure your application instance through the Clerk Dashboard for the enterprise connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/authentication/enterprise-connections/overview) to learn how to configure your instance. +You must configure your application instance through the Clerk Dashboard for the enterprise connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/enterprise-connections/overview) to learn how to configure your instance. ## Create the sign-up and sign-in flow diff --git a/docs/custom-flows/forgot-password.mdx b/docs/custom-flows/forgot-password.mdx index 2c27c0dc57..bc66ead0c0 100644 --- a/docs/custom-flows/forgot-password.mdx +++ b/docs/custom-flows/forgot-password.mdx @@ -7,7 +7,7 @@ description: Learn how to build a custom flow using Clerk's API to reset a user' The password reset flow works as follows: -1. Users can have an email address or phone number, or both, as an [identifier](/docs/authentication/configuration/sign-up-sign-in-options#identifiers). The user enters their email address or phone number and asks for a password reset code. +1. Users can have an email address or phone number, or both, as an [identifier](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#identifiers). The user enters their email address or phone number and asks for a password reset code. 1. Clerk sends an email or SMS to the user, containing a code. 1. The user enters the code and a new password. 1. Clerk verifies the code, and if successful, updates the user's password and signs them in. diff --git a/docs/custom-flows/google-one-tap.mdx b/docs/custom-flows/google-one-tap.mdx index dd756742c4..68b68c93e8 100644 --- a/docs/custom-flows/google-one-tap.mdx +++ b/docs/custom-flows/google-one-tap.mdx @@ -12,7 +12,7 @@ This guide will walk you through how to build a custom Google One Tap authentica ## Enable Google as a social connection - To use Google One Tap with Clerk, follow the steps in the [dedicated guide](/docs/authentication/social-connections/google#configure-for-your-production-instance) to configure Google as a social connection in the Clerk Dashboard using custom credentials. + To use Google One Tap with Clerk, follow the steps in the [dedicated guide](/docs/guides/configure/auth-strategies/social-connections/google#configure-for-your-production-instance) to configure Google as a social connection in the Clerk Dashboard using custom credentials. ## Create the Google One Tap authentication flow diff --git a/docs/custom-flows/manage-sms-based-mfa.mdx b/docs/custom-flows/manage-sms-based-mfa.mdx index 7bff2b3a4e..9eb26413d3 100644 --- a/docs/custom-flows/manage-sms-based-mfa.mdx +++ b/docs/custom-flows/manage-sms-based-mfa.mdx @@ -5,7 +5,7 @@ description: Learn how to use the Clerk API to build a custom flow for managing -[Multi-factor verification (MFA)](/docs/authentication/configuration/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. +[Multi-factor verification (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. One of the options that Clerk supports for MFA is **SMS verification codes**. This guide will walk you through how to build a custom flow that allows users to manage their TOTP settings. diff --git a/docs/custom-flows/manage-sso-connections.mdx b/docs/custom-flows/manage-sso-connections.mdx index 1cf6ce8290..e6956e1943 100644 --- a/docs/custom-flows/manage-sso-connections.mdx +++ b/docs/custom-flows/manage-sso-connections.mdx @@ -11,15 +11,15 @@ This guide demonstrates how to build a custom user interface that allows users t You must configure your application instance through the Clerk Dashboard for the SSO connections that you want to use. -- For social (OAuth) connection(s), see [the appropriate guide for your platform](/docs/authentication/social-connections/overview). -- For enterprise connection(s), see [the appropriate guide for your platform](/docs/authentication/enterprise-connections/overview). +- For social (OAuth) connection(s), see [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/social-connections/overview). +- For enterprise connection(s), see [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/enterprise-connections/overview). This guide uses Discord, Google, and GitHub as examples. ## Build the custom flow 1. The [`useUser()`](/docs/hooks/use-user) hook is used to get the current user's [`User`](/docs/references/javascript/user) object. The `isLoaded` boolean is used to ensure that Clerk is loaded. -1. The `options` array is used to create a list of supported SSO connections. This example uses [OAuth strategies](/docs/authentication/social-connections/oauth). You can edit this array to include all of the SSO connections that you've enabled for your app in the Clerk Dashboard. You can also add [custom SSO connections](/docs/authentication/social-connections/custom-provider) by using the `oauth_custom_` strategy. +1. The `options` array is used to create a list of supported SSO connections. This example uses [OAuth strategies](/docs/guides/configure/auth-strategies/social-connections/oauth). You can edit this array to include all of the SSO connections that you've enabled for your app in the Clerk Dashboard. You can also add [custom SSO connections](/docs/guides/configure/auth-strategies/social-connections/custom-provider) by using the `oauth_custom_` strategy. 1. The `addSSO()` function is used to add a new external account using the `strategy` that is passed in. - It uses the `user` object to access the [`createExternalAccount()`](/docs/references/javascript/user#create-external-account) method. - The `createExternalAccount()` method is used to create a new external account using the `strategy` that is passed in. It's passed to the [`useReverification()`](/docs/hooks/use-reverification) hook to require the user to reverify their credentials before being able to add an external account to their account. diff --git a/docs/custom-flows/manage-totp-based-mfa.mdx b/docs/custom-flows/manage-totp-based-mfa.mdx index aa3039e6d9..351e7c314a 100644 --- a/docs/custom-flows/manage-totp-based-mfa.mdx +++ b/docs/custom-flows/manage-totp-based-mfa.mdx @@ -5,7 +5,7 @@ description: Learn how to use the Clerk API to build a custom flow for managing -[Multi-factor verification (MFA)](/docs/authentication/configuration/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. +[Multi-factor verification (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. One of the options that Clerk supports for MFA is **Authenticator applications (also known as TOTP - Time-based One-time Password)**. This guide will walk you through how to build a custom flow that allows users to manage their TOTP settings. diff --git a/docs/custom-flows/multi-session-applications.mdx b/docs/custom-flows/multi-session-applications.mdx index 92a62712ae..bb48ba4454 100644 --- a/docs/custom-flows/multi-session-applications.mdx +++ b/docs/custom-flows/multi-session-applications.mdx @@ -89,7 +89,7 @@ To add a new session, simply link to your existing sign-in flow. New sign-ins wi - [Email and password](/docs/custom-flows/email-password) - [Passwordless authentication](/docs/custom-flows/email-sms-otp) -- [Social sign-in (OAuth)](/docs/authentication/social-connections/overview) +- [Social sign-in (OAuth)](/docs/guides/configure/auth-strategies/social-connections/overview) For more information on how Clerk's sign-in flow works, see the [detailed sign-in guide](/docs/custom-flows/overview#sign-in-flow). diff --git a/docs/custom-flows/oauth-connections.mdx b/docs/custom-flows/oauth-connections.mdx index 03372e2565..e6f55a3bb9 100644 --- a/docs/custom-flows/oauth-connections.mdx +++ b/docs/custom-flows/oauth-connections.mdx @@ -7,7 +7,7 @@ description: Learn how to use the Clerk API to build a custom sign-up and sign-i ## Before you start -You must configure your application instance through the Clerk Dashboard for the social connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/authentication/social-connections/oauth) to learn how to configure your instance. +You must configure your application instance through the Clerk Dashboard for the social connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/social-connections/oauth) to learn how to configure your instance. ## Create the sign-up and sign-in flow diff --git a/docs/custom-flows/overview.mdx b/docs/custom-flows/overview.mdx index e51660158b..b761c5f286 100644 --- a/docs/custom-flows/overview.mdx +++ b/docs/custom-flows/overview.mdx @@ -16,7 +16,7 @@ Before building custom authentication flows, read the following sections to get The [`SignUp`](/docs/references/javascript/sign-up) object is the pivotal concept in the sign-up process. It is used to gather the user's information, verify their email address or phone number, add OAuth accounts, and finally, convert them into a [`User`](/docs/references/javascript/user). -Every `SignUp` must meet specific requirements before being converted into a `User`. These requirements are defined by the instance settings you selected in the [Clerk Dashboard](https://dashboard.clerk.com/). For example, on the [**Email, phone, username**](https://dashboard.clerk.com/last-active?path=user-authentication/email-phone-username) page, you can [configure email and password, email links, or SMS OTP as authentication strategies](/docs/authentication/configuration/sign-up-sign-in-options). +Every `SignUp` must meet specific requirements before being converted into a `User`. These requirements are defined by the instance settings you selected in the [Clerk Dashboard](https://dashboard.clerk.com/). For example, on the [**Email, phone, username**](https://dashboard.clerk.com/last-active?path=user-authentication/email-phone-username) page, you can [configure email and password, email links, or SMS OTP as authentication strategies](/docs/guides/configure/auth-strategies/sign-up-sign-in-options). Once all requirements are met, the `SignUp` will turn into a new `User`, and an active session for that `User` will be created on the current [`Client`](/docs/references/javascript/client). @@ -82,7 +82,7 @@ The following steps outline the sign-in process: 1. Initiate the sign-in process by collecting the user's authentication information and passing the appropriate parameters to the [`create()`](/docs/references/javascript/sign-in#create) method. 1. Prepare the first factor verification. Users must complete a first factor verification to prove their identity. This can be something like providing a password, an email link, a one-time code (OTP), a Web3 wallet address, or providing proof of their identity through an external social account (SSO/OAuth). 1. Attempt to complete the first factor verification. -1. Optionally, if you have enabled [multi-factor](/docs/authentication/configuration/sign-up-sign-in-options) for your application, you will need to prepare the second factor verification for users who have set up 2FA for their account. +1. Optionally, if you have enabled [multi-factor](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) for your application, you will need to prepare the second factor verification for users who have set up 2FA for their account. 1. Attempt to complete the second factor verification. 1. If verification is successful, set the newly created session as the active session by passing the `SignIn.createdSessionId` to the [`setActive()`](/docs/references/javascript/clerk#set-active) method on the `Clerk` object. diff --git a/docs/custom-flows/passkeys.mdx b/docs/custom-flows/passkeys.mdx index 8b7b208ff9..12db370a31 100644 --- a/docs/custom-flows/passkeys.mdx +++ b/docs/custom-flows/passkeys.mdx @@ -5,7 +5,7 @@ description: Learn how to use the Clerk API to build a custom authentication flo -Clerk supports passwordless authentication via [passkeys](/docs/authentication/configuration/sign-up-sign-in-options#passkeys), enabling users to sign in without having to remember a password. Instead, users select a passkey associated with their device, which they can use to authenticate themselves. +Clerk supports passwordless authentication via [passkeys](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#passkeys), enabling users to sign in without having to remember a password. Instead, users select a passkey associated with their device, which they can use to authenticate themselves. This guide demonstrates how to use the Clerk API to build a custom user interface for creating, signing users in with, and managing passkeys. diff --git a/docs/customization/elements/reference/common.mdx b/docs/customization/elements/reference/common.mdx index d16278b08e..9fea5a5d37 100644 --- a/docs/customization/elements/reference/common.mdx +++ b/docs/customization/elements/reference/common.mdx @@ -13,7 +13,7 @@ Unless otherwise mentioned, all components accept a `className` prop. ## `` -Renders a social connection button based on the provided `name`. Throws an error in development if the provided social connection is not enabled in your instance. See [Social connections (OAuth)](/docs/authentication/social-connections/overview) to learn how to enable them in the Clerk Dashboard. +Renders a social connection button based on the provided `name`. Throws an error in development if the provided social connection is not enabled in your instance. See [Social connections (OAuth)](/docs/guides/configure/auth-strategies/social-connections/overview) to learn how to enable them in the Clerk Dashboard. By default, `` will be rendered as an unstyled `