From 4d5b9447a8119e0e99f676c9da4014a2a6a8e809 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 9 Jul 2025 16:32:25 -0400 Subject: [PATCH 01/11] validate and copy over tooltips to dist --- scripts/build-docs.ts | 68 ++++++++++++++++++++++- scripts/lib/config.ts | 15 +++++ scripts/lib/error-messages.ts | 4 ++ scripts/lib/io.ts | 5 ++ scripts/lib/store.ts | 32 +++++++++++ scripts/lib/tooltips.ts | 101 ++++++++++++++++++++++++++++++++++ 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 scripts/lib/tooltips.ts diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index a00f16a691..dba951b186 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -22,7 +22,7 @@ // Transforms // - Content Integration: -// - Embeds partial content into markdown files +// - Embeds partial and tooltip content into markdown files // - Embeds typedoc content where referenced // - Handles special character encoding in typedoc tables // - Link Processing: @@ -85,6 +85,7 @@ import { type Prompt, readPrompts, writePrompts, checkPrompts } from './lib/prom 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' // 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) { @@ -118,6 +119,10 @@ async function main() { inputPath: '../prompts', outputPath: '_prompts', }, + tooltips: { + inputPath: '../docs/_tooltips', + outputPath: '_tooltips', + }, ignoreLinks: ['/docs/quickstart'], ignorePaths: [ '/docs/core-1', @@ -149,6 +154,7 @@ async function main() { 'types/organization-custom-role-key.mdx': ['link-doc-not-found'], }, partials: {}, + tooltips: {}, }, validSdks: VALID_SDKS, manifestOptions: { @@ -195,6 +201,8 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const getDocsFolder = readDocsFolder(config) const getPartialsFolder = readPartialsFolder(config) const getPartialsMarkdown = readPartialsMarkdown(config, store) + const getTooltipsFolder = readTooltipsFolder(config) + const getTooltipsMarkdown = readTooltipsMarkdown(config, store) const getTypedocsFolder = readTypedocsFolder(config) const getTypedocsMarkdown = readTypedocsMarkdown(config, store) const parseMarkdownFile = parseInMarkdownFile(config, store) @@ -205,6 +213,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const getCommitDate = getLastCommitDate(config) const markDirty = markDocumentDirty(store) const scopeHref = scopeHrefToSDK(config) + const writeTooltipsToDist = writeTooltips(config, store) abortSignal?.throwIfAborted() @@ -259,6 +268,10 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) + const cachedTooltipsSize = store.tooltips.size + const tooltips = await getTooltipsMarkdown((await getTooltipsFolder()).map((item) => item.path)) + console.info(`✓ Loaded in ${tooltips.length} tooltips (${cachedTooltipsSize} cached)`) + abortSignal?.throwIfAborted() const cachedTypedocsSize = store.typedocs.size @@ -515,6 +528,50 @@ export async function build(config: BuildConfig, store: Store = createBlankStore abortSignal?.throwIfAborted() + const validatedTooltips = await Promise.all( + tooltips.map(async (tooltip) => { + if (config.tooltips === null) { + throw new Error('Tooltips are not enabled') + } + + const tooltipPath = `${config.tooltips.inputPathRelative}/${tooltip.path}` + + try { + let node: Node | null = null + const links: Set = new Set() + + const vfile = await remark() + .use(remarkMdx) + .use( + validateAndEmbedLinks(config, docsMap, tooltipPath, 'tooltips', (linkInTooltip) => { + links.add(linkInTooltip) + }), + ) + .use(() => (tree, vfile) => { + node = tree + }) + .process(tooltip.vfile) + + if (node === null) { + throw new Error(errorMessages['tooltip-parse-error'](tooltip.path)) + } + + return { + ...tooltip, + vfile, + node: node as Node, + links, + } + } catch (error) { + console.error(`✗ Error validating tooltip: ${tooltip.path}`) + throw error + } + }), + ) + console.info(`✓ Validated all tooltips`) + + abortSignal?.throwIfAborted() + const validatedTypedocs = await Promise.all( typedocs.map(async (typedoc) => { const filePath = path.join(config.typedocRelativePath, typedoc.path) @@ -883,6 +940,13 @@ template: wide abortSignal?.throwIfAborted() + if (config.tooltips) { + await writeTooltipsToDist(validatedTooltips) + console.info(`✓ Wrote ${validatedTooltips.length} tooltips to disk`) + } + + abortSignal?.throwIfAborted() + if (config.llms?.fullPath || config.llms?.overviewPath) { const outputtedDocsFiles = listOutputDocsFiles(config, store.writtenFiles, mdxFilePaths) @@ -905,12 +969,14 @@ template: wide const coreVFiles = coreDocs.map((doc) => doc.vfile) const partialsVFiles = validatedPartials.map((partial) => partial.vfile) + const tooltipsVFiles = validatedTooltips.map((tooltip) => tooltip.vfile) const typedocVFiles = validatedTypedocs.map((typedoc) => typedoc.vfile) const warnings = reporter( [ ...coreVFiles, ...partialsVFiles, + ...tooltipsVFiles, ...typedocVFiles, ...flatSdkSpecificVFiles, manifestVfile, diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index 4a6a7e089d..f16f077ad0 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -23,6 +23,7 @@ type BuildConfigOptions = { docs: Record partials: Record typedoc: Record + tooltips: Record } manifestOptions: { wrapDefault: boolean @@ -43,6 +44,10 @@ type BuildConfigOptions = { inputPath: string outputPath: string } + tooltips?: { + inputPath: string + outputPath: string + } llms?: { overviewPath?: string fullPath?: string @@ -102,6 +107,7 @@ export async function createConfig(config: BuildConfigOptions) { docs: {}, partials: {}, typedoc: {}, + tooltips: {}, }, manifestOptions: config.manifestOptions ?? { @@ -132,6 +138,15 @@ export async function createConfig(config: BuildConfigOptions) { } : null, + tooltips: config.tooltips + ? { + inputPath: resolve(path.join(config.basePath, config.tooltips.inputPath)), + inputPathRelative: config.tooltips.inputPath, + outputPath: resolve(path.join(tempDist, config.tooltips.outputPath)), + outputPathRelative: config.tooltips.outputPath, + } + : null, + llms: config.llms ? { overviewPath: config.llms.overviewPath, diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts index 5f560362b8..a896e6f943 100644 --- a/scripts/lib/error-messages.ts +++ b/scripts/lib/error-messages.ts @@ -74,6 +74,10 @@ export const errorMessages = { 'markdown-read-error': (href: string): string => `Attempting to read in ${href}.mdx failed`, 'partial-parse-error': (path: string): string => `Failed to parse the content of ${path}`, + // Tooltip errors + 'tooltip-read-error': (path: string): string => `Failed to read in ${path} from tooltips file`, + 'tooltip-parse-error': (path: string): string => `Failed to parse the content of ${path}`, + // Typedoc errors 'typedoc-folder-not-found': (path: string): string => `Typedoc folder ${path} not found, run "npm run typedoc:download"`, diff --git a/scripts/lib/io.ts b/scripts/lib/io.ts index 597c5141ba..a30eac9e7e 100644 --- a/scripts/lib/io.ts +++ b/scripts/lib/io.ts @@ -24,6 +24,11 @@ export const readDocsFolder = (config: BuildConfig) => async () => { fileFilter: (entry) => // Partials are inside the docs folder, so we need to exclude them `${config.docsRelativePath}/${entry.path}`.startsWith(config.partialsRelativePath) === false && + // Tooltips are inside the docs folder too, also ignore them as they are not full pages + (config.tooltips?.inputPathRelative + ? `${config.docsRelativePath}/${entry.path}`.startsWith(config.tooltips.inputPathRelative) === false + : true) && + // Ignore anything that isn't an .mdx file entry.path.endsWith('.mdx'), }) diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts index 8a3ad636d3..88a658af08 100644 --- a/scripts/lib/store.ts +++ b/scripts/lib/store.ts @@ -9,22 +9,26 @@ import type { BuildConfig } from './config' import type { parseInMarkdownFile } from './markdown' import type { readPartial } from './partials' import type { readTypedoc } from './typedoc' +import { readTooltip } from './tooltips' type MarkdownFile = Awaited>> type CoreDocsFile = VFile type PartialsFile = Awaited>> type TypedocsFile = Awaited>> +type TooltipsFile = Awaited>> export type DocsMap = Map export type CoreDocsMap = Map export type PartialsMap = Map export type TypedocsMap = Map +export type TooltipsMap = Map export const createBlankStore = () => ({ markdown: new Map() as DocsMap, coreDocs: new Map() as CoreDocsMap, partials: new Map() as PartialsMap, typedocs: new Map() as TypedocsMap, + tooltips: new Map() as TooltipsMap, dirtyDocMap: new Map() as Map>, writtenFiles: new Map() as Map, }) @@ -81,6 +85,23 @@ export const invalidateFile = }) } } + + if (config.tooltips) { + const relativeTooltipPath = path.relative(config.tooltips.inputPath, filePath) + + if (store.tooltips.has(relativeTooltipPath)) { + store.tooltips.delete(relativeTooltipPath) + + const adjacent = store.dirtyDocMap.get(relativeTooltipPath) + + if (adjacent && invalidateAdjacentDocs) { + const invalidate = invalidateFile(store, config) + adjacent.forEach((docPath) => { + invalidate(docPath, false) + }) + } + } + } } export const markDocumentDirty = @@ -123,6 +144,17 @@ export const getPartialsCache = (store: Store) => { } } +export const getTooltipsCache = (store: Store) => { + return async (key: string, cacheMiss: (key: string) => Promise) => { + const cached = store.tooltips.get(key) + if (cached) return structuredClone(cached) + + const result = await cacheMiss(key) + store.tooltips.set(key, structuredClone(result)) + return result + } +} + export const getTypedocsCache = (store: Store) => { return async (key: string, cacheMiss: (key: string) => Promise) => { const cached = store.typedocs.get(key) diff --git a/scripts/lib/tooltips.ts b/scripts/lib/tooltips.ts new file mode 100644 index 0000000000..84bc1f62d1 --- /dev/null +++ b/scripts/lib/tooltips.ts @@ -0,0 +1,101 @@ +// responsible for reading in and parsing the partials markdown +// for validation see validators/checkPartials.ts +// for partials we currently do not allow them to embed other partials +// this also removes the .mdx suffix from the urls in the markdown + +import path from 'node:path' +import readdirp from 'readdirp' +import { remark } from 'remark' +import remarkFrontmatter from 'remark-frontmatter' +import remarkMdx from 'remark-mdx' +import type { Node } from 'unist' +import reporter from 'vfile-reporter' +import type { BuildConfig } from './config' +import { errorMessages } from './error-messages' +import { readMarkdownFile, writeDistFile } from './io' +import { removeMdxSuffixPlugin } from './plugins/removeMdxSuffixPlugin' +import { getTooltipsCache, type Store } from './store' + +export const readTooltipsFolder = (config: BuildConfig) => async () => { + if (!config.tooltips) { + throw new Error('Tooltips are not enabled') + } + + return readdirp.promise(config.tooltips.inputPath, { + type: 'files', + fileFilter: '*.mdx', + }) +} + +export const readTooltip = (config: BuildConfig) => async (filePath: string) => { + if (!config.tooltips) { + throw new Error('Tooltips are not enabled') + } + + const fullPath = path.join(config.tooltips.inputPath, filePath) + + const [error, content] = await readMarkdownFile(fullPath) + + if (error) { + throw new Error(errorMessages['tooltip-read-error'](fullPath), { cause: error }) + } + + let tooltipNode: Node | null = null + + try { + const tooltipContentVFile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .use(() => (tree) => { + tooltipNode = tree + }) + .use(removeMdxSuffixPlugin(config)) + .process({ + path: `docs/_tooltips/${filePath}`, + value: content, + }) + + const tooltipContentReport = reporter([tooltipContentVFile], { quiet: true }) + + if (tooltipContentReport !== '') { + console.error(tooltipContentReport) + process.exit(1) + } + + if (tooltipNode === null) { + throw new Error(errorMessages['tooltip-parse-error'](filePath)) + } + + return { + path: `_tooltips/${filePath}`, + content, + vfile: tooltipContentVFile, + node: tooltipNode as Node, + } + } catch (error) { + console.error(`✗ Error parsing tooltip: ${filePath}`) + throw error + } +} + +export const readTooltipsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { + const read = readTooltip(config) + const tooltipsCache = getTooltipsCache(store) + + return Promise.all(paths.map(async (markdownPath) => tooltipsCache(markdownPath, () => read(markdownPath)))) +} + +type Tooltips = Awaited>> + +export const writeTooltips = (config: BuildConfig, store: Store) => async (tooltips: Tooltips) => { + if (!config.tooltips) { + throw new Error('Tooltips are not enabled') + } + + const write = writeDistFile(config, store) + + for (const tooltip of tooltips) { + await write(tooltip.path, tooltip.content) + console.info(`✓ Wrote tooltip: ${tooltip.path}`) + } +} From cef633137027bb9335c57d02af7458a3c0119043 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 9 Jul 2025 22:59:02 -0400 Subject: [PATCH 02/11] fix tests --- scripts/lib/tooltips.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/lib/tooltips.ts b/scripts/lib/tooltips.ts index 84bc1f62d1..665131004a 100644 --- a/scripts/lib/tooltips.ts +++ b/scripts/lib/tooltips.ts @@ -18,7 +18,8 @@ import { getTooltipsCache, type Store } from './store' export const readTooltipsFolder = (config: BuildConfig) => async () => { if (!config.tooltips) { - throw new Error('Tooltips are not enabled') + console.error('Tooltips are not enabled') + return [] } return readdirp.promise(config.tooltips.inputPath, { From 525bbdb11ad2cf088c7e59efc719a6bbf2ad8fa2 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 9 Jul 2025 23:58:52 -0400 Subject: [PATCH 03/11] write out the processed vfile --- scripts/lib/tooltips.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/lib/tooltips.ts b/scripts/lib/tooltips.ts index 665131004a..711af0cc0a 100644 --- a/scripts/lib/tooltips.ts +++ b/scripts/lib/tooltips.ts @@ -96,7 +96,6 @@ export const writeTooltips = (config: BuildConfig, store: Store) => async (toolt const write = writeDistFile(config, store) for (const tooltip of tooltips) { - await write(tooltip.path, tooltip.content) - console.info(`✓ Wrote tooltip: ${tooltip.path}`) + await write(tooltip.path, tooltip.vfile.value as string) } } From 772b61073a58af804b48fe9d1add110261ddb628 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Jul 2025 00:24:02 -0400 Subject: [PATCH 04/11] update comment --- scripts/lib/tooltips.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/lib/tooltips.ts b/scripts/lib/tooltips.ts index 711af0cc0a..3a740c00a2 100644 --- a/scripts/lib/tooltips.ts +++ b/scripts/lib/tooltips.ts @@ -1,6 +1,6 @@ -// responsible for reading in and parsing the partials markdown -// for validation see validators/checkPartials.ts -// for partials we currently do not allow them to embed other partials +// responsible for reading in and parsing the tooltips markdown +// for validation see validators/checkTooltips.ts +// for tooltips we currently do not allow them to embed other tooltips // this also removes the .mdx suffix from the urls in the markdown import path from 'node:path' From b10d114ca4876440e0b81ac378ca335e18e62374 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Jul 2025 00:25:04 -0400 Subject: [PATCH 05/11] embed tooltips --- scripts/build-docs.ts | 18 +++++- scripts/lib/error-messages.ts | 2 + scripts/lib/markdown.ts | 7 ++ scripts/lib/plugins/checkTooltips.ts | 97 ++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 scripts/lib/plugins/checkTooltips.ts diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index dba951b186..11da23e40e 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 { checkTooltips } from './lib/plugins/checkTooltips' // 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) { @@ -306,7 +307,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const inManifest = docsInManifest.has(file.href) const markdownFile = await markdownCache(file.filePath, () => - parseMarkdownFile(file, partials, typedocs, prompts, inManifest, 'docs'), + parseMarkdownFile(file, partials, tooltips, typedocs, prompts, inManifest, 'docs'), ) docsMap.set(file.href, markdownFile) @@ -317,7 +318,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const inManifest = docsInManifest.has(file.href) const markdownFile = await markdownCache(file.filePath, () => - parseMarkdownFile(file, partials, typedocs, prompts, inManifest, 'docs'), + parseMarkdownFile(file, partials, tooltips, typedocs, prompts, inManifest, 'docs'), ) docsMap.set(file.href, markdownFile) @@ -686,6 +687,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const foundLinks: Set = new Set() const foundPartials: Set = new Set() const foundTypedocs: Set = new Set() + const foundTooltips: Set = new Set() const vfile = await coreDocCache(doc.file.filePath, async () => remark() @@ -708,6 +710,11 @@ export async function build(config: BuildConfig, store: Store = createBlankStore foundPartials.add(partial) }), ) + .use( + checkTooltips(config, validatedTooltips, doc.file, { reportWarnings: false, embed: true }, (tooltip) => { + foundTooltips.add(tooltip) + }), + ) .use( checkTypedoc( config, @@ -737,7 +744,11 @@ export async function build(config: BuildConfig, store: Store = createBlankStore .filter((typedoc) => foundTypedocs.has(typedoc.path)) .reduce((acc, { links }) => new Set([...acc, ...links]), foundTypedocs) - const allLinks = new Set([...foundLinks, ...partialsLinks, ...typedocsLinks]) + const tooltipsLinks = validatedTooltips + .filter((tooltip) => foundTooltips.has(tooltip.path)) + .reduce((acc, { links }) => new Set([...acc, ...links]), foundTooltips) + + const allLinks = new Set([...foundLinks, ...partialsLinks, ...typedocsLinks, ...tooltipsLinks]) allLinks.forEach((link) => { markDirty(doc.file.filePath, link) @@ -798,6 +809,7 @@ template: wide .use(remarkMdx) .use(validateAndEmbedLinks(config, docsMap, doc.file.filePath, 'docs', undefined, doc.file.href)) .use(checkPartials(config, partials, doc.file, { reportWarnings: true, embed: true })) + .use(checkTooltips(config, tooltips, doc.file, { reportWarnings: true, embed: true })) .use(checkTypedoc(config, typedocs, doc.file.filePath, { reportWarnings: true, embed: true })) .use(checkPrompts(config, prompts, doc.file, { reportWarnings: true, update: true })) .use(filterOtherSDKsContentOut(config, doc.file.filePath, targetSdk)) diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts index a896e6f943..aa3e807509 100644 --- a/scripts/lib/error-messages.ts +++ b/scripts/lib/error-messages.ts @@ -77,6 +77,8 @@ export const errorMessages = { // Tooltip errors 'tooltip-read-error': (path: string): string => `Failed to read in ${path} from tooltips file`, 'tooltip-parse-error': (path: string): string => `Failed to parse the content of ${path}`, + 'tooltip-src-not-tooltip': (): string => ` prop "src" must start with "_tooltips/"`, + 'tooltip-not-found': (src: string): string => `Tooltip ${src} not found`, // Typedoc errors 'typedoc-folder-not-found': (path: string): string => diff --git a/scripts/lib/markdown.ts b/scripts/lib/markdown.ts index 9aab66b529..941d87dd36 100644 --- a/scripts/lib/markdown.ts +++ b/scripts/lib/markdown.ts @@ -26,6 +26,7 @@ import { documentHasIfComponents } from './utils/documentHasIfComponents' import { extractHeadingFromHeadingNode } from './utils/extractHeadingFromHeadingNode' import { Prompt, checkPrompts } from './prompts' import { markDocumentDirty, type Store } from './store' +import { checkTooltips } from './plugins/checkTooltips' const calloutRegex = new RegExp(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|QUIZ)(\s+[0-9a-z-]+)?\]$/) @@ -34,6 +35,7 @@ export const parseInMarkdownFile = async ( file: DocsFile & { content?: string }, partials: { path: string; content: string; node: Node }[], + tooltips: { path: string; content: string; node: Node }[], typedocs: { path: string; content: string; node: Node }[], prompts: Prompt[], inManifest: boolean, @@ -78,6 +80,11 @@ export const parseInMarkdownFile = markDirty(file.filePath, partial) }), ) + .use( + checkTooltips(config, tooltips, file, { reportWarnings: true, embed: false }, (tooltip) => { + markDirty(file.filePath, tooltip) + }), + ) .use( checkTypedoc(config, typedocs, file.filePath, { reportWarnings: true, embed: false }, (typedoc) => { markDirty(file.filePath, typedoc) diff --git a/scripts/lib/plugins/checkTooltips.ts b/scripts/lib/plugins/checkTooltips.ts new file mode 100644 index 0000000000..1f039f1dab --- /dev/null +++ b/scripts/lib/plugins/checkTooltips.ts @@ -0,0 +1,97 @@ +// This validator manages the tooltips in the docs +// based on the options passed through it can +// - only report warnings if something ain't right +// - only embed the tooltips contents in to the markdown +// - both report warnings and embed the tooltips contents + +import type { Node } from 'unist' +import { map as mdastMap } from 'unist-util-map' +import type { VFile } from 'vfile' +import type { BuildConfig } from '../config' +import { safeMessage } from '../error-messages' +import type { DocsFile } from '../io' +import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' +import { removeMdxSuffix } from '../utils/removeMdxSuffix' +import { z } from 'zod' +import { u as mdastBuilder } from 'unist-builder' + +export const checkTooltips = + ( + config: BuildConfig, + tooltips: { + node: Node + path: string + }[], + file: DocsFile, + options: { + reportWarnings: boolean + embed: boolean + }, + foundTooltip?: (tooltip: string) => void, + ) => + () => + (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + const tooltipSrc = extractComponentPropValueFromNode( + config, + node, + vfile, + 'Tooltip', + 'src', + true, + 'docs', + file.filePath, + z.string(), + ) + + if (tooltipSrc === undefined) return node + + if (tooltipSrc.startsWith('_tooltips/') === false) { + if (options.reportWarnings === true) { + safeMessage(config, vfile, file.filePath, 'docs', 'tooltip-src-not-tooltip', [], node.position) + } + return node + } + + const tooltip = tooltips.find((tooltip) => tooltip.path === `${removeMdxSuffix(tooltipSrc)}.mdx`) + + if (tooltip === undefined) { + if (options.reportWarnings === true) { + safeMessage( + config, + vfile, + file.filePath, + 'docs', + 'tooltip-not-found', + [removeMdxSuffix(tooltipSrc)], + node.position, + ) + } + return node + } + + foundTooltip?.(`${removeMdxSuffix(tooltipSrc)}.mdx`) + + if (options.embed === true) { + return Object.assign( + node, + mdastBuilder('mdxJsxTextElement', { + name: 'Tooltip', + attributes: [], + children: [ + mdastBuilder('mdxJsxTextElement', { + name: 'TooltipTitle', + children: (node as any).children, + }), + mdastBuilder('mdxJsxTextElement', { + name: 'TooltipDescription', + children: [tooltip.node], + }), + ], + }), + ) + } + + return node + }) + } From afb066b500c1c5a9e43a43c65c3a8c29f60078cc Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Jul 2025 00:32:52 -0400 Subject: [PATCH 06/11] remove need to write out tooltips to dist --- scripts/build-docs.ts | 10 +--------- scripts/lib/tooltips.ts | 14 -------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 11da23e40e..c37928d233 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -85,7 +85,7 @@ import { type Prompt, readPrompts, writePrompts, checkPrompts } from './lib/prom 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 { readTooltipsFolder, readTooltipsMarkdown } from './lib/tooltips' import { checkTooltips } from './lib/plugins/checkTooltips' // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts @@ -214,7 +214,6 @@ export async function build(config: BuildConfig, store: Store = createBlankStore const getCommitDate = getLastCommitDate(config) const markDirty = markDocumentDirty(store) const scopeHref = scopeHrefToSDK(config) - const writeTooltipsToDist = writeTooltips(config, store) abortSignal?.throwIfAborted() @@ -952,13 +951,6 @@ template: wide abortSignal?.throwIfAborted() - if (config.tooltips) { - await writeTooltipsToDist(validatedTooltips) - console.info(`✓ Wrote ${validatedTooltips.length} tooltips to disk`) - } - - abortSignal?.throwIfAborted() - if (config.llms?.fullPath || config.llms?.overviewPath) { const outputtedDocsFiles = listOutputDocsFiles(config, store.writtenFiles, mdxFilePaths) diff --git a/scripts/lib/tooltips.ts b/scripts/lib/tooltips.ts index 3a740c00a2..0537d3d870 100644 --- a/scripts/lib/tooltips.ts +++ b/scripts/lib/tooltips.ts @@ -85,17 +85,3 @@ export const readTooltipsMarkdown = (config: BuildConfig, store: Store) => async return Promise.all(paths.map(async (markdownPath) => tooltipsCache(markdownPath, () => read(markdownPath)))) } - -type Tooltips = Awaited>> - -export const writeTooltips = (config: BuildConfig, store: Store) => async (tooltips: Tooltips) => { - if (!config.tooltips) { - throw new Error('Tooltips are not enabled') - } - - const write = writeDistFile(config, store) - - for (const tooltip of tooltips) { - await write(tooltip.path, tooltip.vfile.value as string) - } -} From 34e6c276de0948779bc67a924047eecc24b70002 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Jul 2025 20:27:07 +0800 Subject: [PATCH 07/11] Update component names to match expectations https://github.com/clerk/clerk/pull/1359#issuecomment-3113224305 --- scripts/lib/plugins/checkTooltips.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lib/plugins/checkTooltips.ts b/scripts/lib/plugins/checkTooltips.ts index 1f039f1dab..3a81dc63b9 100644 --- a/scripts/lib/plugins/checkTooltips.ts +++ b/scripts/lib/plugins/checkTooltips.ts @@ -80,11 +80,11 @@ export const checkTooltips = attributes: [], children: [ mdastBuilder('mdxJsxTextElement', { - name: 'TooltipTitle', + name: 'TooltipTrigger', children: (node as any).children, }), mdastBuilder('mdxJsxTextElement', { - name: 'TooltipDescription', + name: 'TooltipContent', children: [tooltip.node], }), ], From 0d43729bde83af388c9ecc6e765ebbd56487b651 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 6 Aug 2025 20:00:27 +0800 Subject: [PATCH 08/11] Fix cache invalidation getting adjacent files --- scripts/lib/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts index 06b847e83e..e47b81c5d2 100644 --- a/scripts/lib/store.ts +++ b/scripts/lib/store.ts @@ -98,7 +98,7 @@ export const invalidateFile = if (store.tooltips.has(relativeTooltipPath)) { store.tooltips.delete(relativeTooltipPath) - const adjacent = store.dirtyDocMap.get(relativeTooltipPath) + const adjacent = store.dirtyDocMap.get(`_tooltips/${relativeTooltipPath}`) if (adjacent && invalidateAdjacentDocs) { const invalidate = invalidateFile(store, config) From efb890c02a5182ed9452087233ea7dbf5476c469 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:36:50 -0400 Subject: [PATCH 09/11] update tooltip syntax (#2598) --- CONTRIBUTING.md | 14 ++++++ docs/_partials/clerk-middleware-options.mdx | 2 +- docs/_tooltips/active-organization.mdx | 1 + .../resources/session-tokens.mdx | 8 ++-- docs/components/billing/checkout-button.mdx | 2 +- .../billing/subscription-details-button.mdx | 2 +- .../organization/organization-switcher.mdx | 2 +- .../manage-membership-requests.mdx | 2 +- .../manage-organization-invitations.mdx | 6 +-- docs/custom-flows/manage-roles.mdx | 2 +- docs/custom-flows/organization-switcher.mdx | 2 +- docs/custom-flows/update-organizations.mdx | 2 +- docs/guides/authorization-checks.mdx | 2 +- docs/guides/multi-tenant-architecture.mdx | 4 +- docs/hooks/use-organization-list.mdx | 2 +- docs/hooks/use-organization.mdx | 2 +- docs/organizations/create-orgs-for-users.mdx | 2 +- docs/organizations/force-organizations.mdx | 8 ++-- docs/organizations/org-slugs-in-urls.mdx | 4 +- docs/references/astro/auth-store.mdx | 6 +-- docs/references/astro/locals.mdx | 2 +- docs/references/astro/organization-store.mdx | 2 +- docs/references/backend/types/auth-object.mdx | 12 ++--- docs/references/javascript/clerk.mdx | 4 +- docs/references/javascript/organization.mdx | 10 ++-- docs/references/javascript/overview.mdx | 2 +- docs/references/javascript/session.mdx | 4 +- .../javascript/types/set-active-params.mdx | 2 +- docs/references/nextjs/clerk-middleware.mdx | 2 +- docs/references/sdk/terminology.mdx | 2 +- docs/references/vue/use-auth.mdx | 6 +-- docs/references/vue/use-organization.mdx | 4 +- .../core-2/chrome-extension.mdx | 4 +- docs/upgrade-guides/core-2/expo.mdx | 4 +- docs/upgrade-guides/core-2/javascript.mdx | 4 +- docs/upgrade-guides/core-2/nextjs.mdx | 4 +- docs/upgrade-guides/core-2/react.mdx | 4 +- docs/upgrade-guides/core-2/remix.mdx | 4 +- scripts/build-docs.test.ts | 47 +++++++++++++++++++ scripts/lib/plugins/checkTooltips.ts | 42 ++++++++--------- styleguides/STYLEGUIDE.md | 4 +- 41 files changed, 153 insertions(+), 91 deletions(-) create mode 100644 docs/_tooltips/active-organization.mdx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74640f1738..e79a99fad7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -706,6 +706,20 @@ The video below shows what this example looks like once rendered. https://github.com/clerk/clerk-docs/assets/2615508/9b07ba1d-8bb0-498b-935f-432d2d047ab6 +### Tooltips + +A tooltip is content that appears when the user hovers over a word or phrase in order to provide additional information. A common use case is for definitions. + +Tooltips are defined in the `_tooltips` folder and written in MDX, but they do not support custom MDX components, like callouts or `` components. Try to keep the tooltip content as text. + +The tooltip syntax is similar to a link, but with a `!` prefix, as shown in the following example: + +```mdx +The ID of the [active organization](!active-organization) that the user belongs to. +``` + +Tooltips should follow the same styleguide as links - only add them on the first mention of a term and only in the highest heading section. So if a term is mentioned in an H2 section and again in its H3 section, it doesn't need to be added in the H3 section. + ### `` The `` component is used at the beginning of a tutorial-type content page. It accepts the following properties: diff --git a/docs/_partials/clerk-middleware-options.mdx b/docs/_partials/clerk-middleware-options.mdx index 610b93abb6..3174fc4eed 100644 --- a/docs/_partials/clerk-middleware-options.mdx +++ b/docs/_partials/clerk-middleware-options.mdx @@ -46,7 +46,7 @@ The `clerkMiddleware()` function accepts an optional object. The following optio - `organizationSyncOptions?` - [OrganizationSyncOptions](#organization-sync-options) | undefined - Used to activate a specific [organization](/docs/organizations/overview) or [personal account](/docs/organizations/organization-workspaces) based on URL path parameters. If there's a mismatch between the active organization in the session (e.g., as reported by [`auth()`](/docs/references/nextjs/auth)) and the organization indicated by the URL, the middleware will attempt to activate the organization specified in the URL. + Used to activate a specific [organization](/docs/organizations/overview) or [personal account](/docs/organizations/organization-workspaces) based on URL path parameters. If there's a mismatch between the [active organization](!active-organization) in the session (e.g., as reported by [`auth()`](/docs/references/nextjs/auth)) and the organization indicated by the URL, the middleware will attempt to activate the organization specified in the URL. --- diff --git a/docs/_tooltips/active-organization.mdx b/docs/_tooltips/active-organization.mdx new file mode 100644 index 0000000000..91aa710261 --- /dev/null +++ b/docs/_tooltips/active-organization.mdx @@ -0,0 +1 @@ +A user can be a member of multiple organizations, but only one can be active at a time. The **active organization** determines which organization-specific data the user can access and which role and related permissions they have within the organization. diff --git a/docs/backend-requests/resources/session-tokens.mdx b/docs/backend-requests/resources/session-tokens.mdx index 2dee259037..1b4ec0db71 100644 --- a/docs/backend-requests/resources/session-tokens.mdx +++ b/docs/backend-requests/resources/session-tokens.mdx @@ -75,10 +75,10 @@ Read more about Clerk session tokens and how they work in [the guide on how Cler | Claim | Abbreviation expanded | Description | Example | | - | - | - | - | - | `org_id` | organization ID | The ID of the active organization that the user belongs to. | `org_123` | - | `org_permissions` | organization permissions | The permissions of the user in the currently active organization. System permissions are not included in the session token. | `["org:admin:example_permission", "org:member:example_permission"]` | - | `org_slug` | organization slug | The slug of the currently active organization that the user belongs to. | `org-slug` | - | `org_role` | organization role | The role of the user in the currently active organization. | `org:admin` | + | `org_id` | organization ID | The ID of the [active organization](!active-organization) that the user belongs to. | `org_123` | + | `org_permissions` | organization permissions | The permissions of the user in the currently [active organization](!active-organization). System permissions are not included in the session token. | `["org:admin:example_permission", "org:member:example_permission"]` | + | `org_slug` | organization slug | The slug of the currently [active organization](!active-organization) that the user belongs to. | `org-slug` | + | `org_role` | organization role | The role of the user in the currently [active organization](!active-organization). | `org:admin` | The **`act` (actor) claim** is only included if the user is [impersonating](/docs/users/user-impersonation) another user. It's value is an object that contains the following properties: diff --git a/docs/components/billing/checkout-button.mdx b/docs/components/billing/checkout-button.mdx index 8f90c51f9c..1049f9c6af 100644 --- a/docs/components/billing/checkout-button.mdx +++ b/docs/components/billing/checkout-button.mdx @@ -78,7 +78,7 @@ The `` component renders a button that opens the checkout draw ``` -`` will throw an error if the `for` prop is set to `'organization'` and no [active organization](/docs/organizations/overview#active-organization) is set. +`` will throw an error if the `for` prop is set to `'organization'` and no [active organization](!active-organization) is set. ```tsx <> diff --git a/docs/components/billing/subscription-details-button.mdx b/docs/components/billing/subscription-details-button.mdx index 1587aa831e..6065d04e3b 100644 --- a/docs/components/billing/subscription-details-button.mdx +++ b/docs/components/billing/subscription-details-button.mdx @@ -59,7 +59,7 @@ All props are optional. ``` -`` will throw an error if the `for` prop is set to `'organization'` and no [active organization](/docs/organizations/overview#active-organization) is set. +`` will throw an error if the `for` prop is set to `'organization'` and no [active organization](!active-organization) is set. ```tsx <> diff --git a/docs/components/organization/organization-switcher.mdx b/docs/components/organization/organization-switcher.mdx index 02cc847609..1a16c4eeed 100644 --- a/docs/components/organization/organization-switcher.mdx +++ b/docs/components/organization/organization-switcher.mdx @@ -25,7 +25,7 @@ The `` component accepts the following properties, all o - `afterLeaveOrganizationUrl` - `string` - The full URL or path to navigate to after the user leaves the currently active organization. + The full URL or path to navigate to after the user leaves the currently [active organization](!active-organization). --- diff --git a/docs/custom-flows/manage-membership-requests.mdx b/docs/custom-flows/manage-membership-requests.mdx index da464acf76..6442330041 100644 --- a/docs/custom-flows/manage-membership-requests.mdx +++ b/docs/custom-flows/manage-membership-requests.mdx @@ -11,7 +11,7 @@ This guide will demonstrate how to use the Clerk API to build a custom flow for The following example: - 1. Uses the [`useOrganization()`](/docs/hooks/use-organization) hook to get `membershipRequests`, which is a list of the active organization's membership requests. + 1. Uses the [`useOrganization()`](/docs/hooks/use-organization) hook to get `membershipRequests`, which is a list of the [active organization's](!active-organization) membership requests. - `membershipRequests` is an object with `data` that contains an array of [`OrganizationMembershipRequest`](/docs/references/javascript/types/organization-membership-request) objects. - Each `OrganizationMembershipRequest` object has an [`accept()`](/docs/references/javascript/types/organization-membership-request#accept) and [`reject()`](/docs/references/javascript/types/organization-membership-request#reject) method to accept or reject the membership request, respectively. 1. Maps over the `data` array to display the membership requests in a table, providing an "Accept" and "Reject" button for each request that calls the `accept()` and `reject()` methods, respectively. diff --git a/docs/custom-flows/manage-organization-invitations.mdx b/docs/custom-flows/manage-organization-invitations.mdx index 135ced01d8..a217801c6c 100644 --- a/docs/custom-flows/manage-organization-invitations.mdx +++ b/docs/custom-flows/manage-organization-invitations.mdx @@ -20,12 +20,12 @@ This guide will demonstrate how to use the Clerk API to build a custom flow for To invite a user: - 1. Use the [`useOrganization()`](/docs/hooks/use-organization) hook to get `organization`, which is the active organization. + 1. Use the [`useOrganization()`](/docs/hooks/use-organization) hook to get `organization`, which is the [active organization](!active-organization). 1. Use `organization` to call the [`inviteMember()`](/docs/references/javascript/organization#invite-member) method, with the recipient's email address and desired role passed as arguments. To revoke an invitation: - 1. Use the `useOrganization()` hook to get `invitations`, which is a list of invitations for the active organization. + 1. Use the `useOrganization()` hook to get `invitations`, which is a list of invitations for the [active organization](!active-organization). 1. `invitations` is an array of [`OrganizationInvitation`](/docs/references/javascript/types/organization-invitation) objects. Each `OrganizationInvitation` object has a [`revoke()`](/docs/references/javascript/types/organization-invitation#revoke) method that can be called to revoke the invitation. The following example includes: @@ -218,7 +218,7 @@ This guide will demonstrate how to use the Clerk API to build a custom flow for To check if the current user is an organization admin: - 1. Get the active organization's ID from the `clerk` object. + 1. Get the [active organization's](!active-organization) ID from the `clerk` object. 1. Call the [`getOrganizationMemberships()`](/docs/references/javascript/user#get-organization-memberships) method to get a list of organizations that the user is a member of. This method returns `data`, which is an array of `OrganizationMembership` objects. 1. In the list of organizations that the user is a member of, find the `OrganizationMembership` object that has an ID that matches the active organization's ID. 1. Check the `role` property of the `OrganizationMembership` object to see if the user is an admin. diff --git a/docs/custom-flows/manage-roles.mdx b/docs/custom-flows/manage-roles.mdx index 789c07bdf2..1dd61bf035 100644 --- a/docs/custom-flows/manage-roles.mdx +++ b/docs/custom-flows/manage-roles.mdx @@ -13,7 +13,7 @@ This guide will demonstrate how to use the Clerk API to build a custom flow for The following example: - 1. Uses the [`useOrganization()`](/docs/hooks/use-organization) hook to get `memberships`, which is a list of the active organization's memberships. + 1. Uses the [`useOrganization()`](/docs/hooks/use-organization) hook to get `memberships`, which is a list of the [active organization's](!active-organization) memberships. - `memberships` is an object with `data` that contains an array of [`OrganizationMembership`](/docs/references/javascript/types/organization-membership) objects. - Each `OrganizationMembership` object has an [`update()`](/docs/references/javascript/types/organization-membership#update) and [`destroy()`](/docs/references/javascript/types/organization-membership#destroy) method to update the member's role and remove the member from the organization, respectively. 1. Maps over the `data` array to display the memberships in a table, providing an "Update Role" and "Remove Member" button for each membership that calls the `update()` and `destroy()` methods, respectively. diff --git a/docs/custom-flows/organization-switcher.mdx b/docs/custom-flows/organization-switcher.mdx index 11c358d6c0..3785cff13e 100644 --- a/docs/custom-flows/organization-switcher.mdx +++ b/docs/custom-flows/organization-switcher.mdx @@ -14,7 +14,7 @@ This guide will demonstrate how to use the Clerk API to build a custom flow for The following examples: 1. Use the [`useOrganizationList()`](/docs/hooks/use-organization-list) hook to get `memberships`, which is a list of the current user's organization memberships. `memberships` returns `data`, which is an array of [`OrganizationMembership`](/docs/references/javascript/types/organization-membership) objects. - 1. Map over the `data` array to display the user's organization memberships in a table, providing a button that calls `setActive()` to set the selected organization as the active organization. + 1. Map over the `data` array to display the user's organization memberships in a table, providing a button that calls `setActive()` to set the selected organization as the [active organization](!active-organization). - If there are no organizations, the [`` component (custom-flow version, not the Clerk component)](/docs/custom-flows/create-organizations) is rendered to allow the user to create an organization. The difference between the two examples is the parameters passed to the `useOrganizationList()` hook in order to determine how the list is paginated. diff --git a/docs/custom-flows/update-organizations.mdx b/docs/custom-flows/update-organizations.mdx index d72efaaf84..902bd1467e 100644 --- a/docs/custom-flows/update-organizations.mdx +++ b/docs/custom-flows/update-organizations.mdx @@ -68,7 +68,7 @@ This guide will demonstrate how to use Clerk's API to build a custom flow for up - The following example uses the `organization.update()` method to update the active organization's name. To see what other attributes can be updated, see the [`update()` reference doc](/docs/references/javascript/organization#update). + The following example uses the `organization.update()` method to update the [active organization's](!active-organization) name. To see what other attributes can be updated, see the [`update()` reference doc](/docs/references/javascript/organization#update). Use the tabs to view the code necessary for the `index.html` and `main.js` files. diff --git a/docs/guides/authorization-checks.mdx b/docs/guides/authorization-checks.mdx index f4925787fe..065216d596 100644 --- a/docs/guides/authorization-checks.mdx +++ b/docs/guides/authorization-checks.mdx @@ -39,7 +39,7 @@ This guide will show you how to implement authorization checks in order to prote - When doing authorization checks, it's recommended to use permission-based over role-based, and feature-based over plan-based authorization, as these approaches are more granular, flexible, and more secure. - Note: Using `has()` **on the server-side** to check permissions works only with **custom permissions**, as [system permissions](/docs/organizations/roles-permissions#system-permissions) aren't included in the session token claims. To check system permissions, verify the user's role instead. -- Checking for a role or permission depends on the user having an [active organization](/docs/organizations/overview#active-organization). Without an active organization, the authorization checks will likely always evaluate to false by default. +- Checking for a role or permission depends on the user having an [active organization](!active-organization). Without an active organization, the authorization checks will likely always evaluate to false by default. - If you would like to perform role-based authorization checks **without** using Clerk's organizations feature, see [the Role Based Access Control (RBAC) guide](/docs/references/nextjs/basic-rbac). - Permission-based authorization checks link with feature-based authorization checks. This means that if you are checking a custom permission, it will only work if the feature part of the permission key (`org::`) **is a feature included in the organization's active plan**. For example, say you want to check if an organization member has the custom permission `org:teams:manage`, where `teams` is the feature. Before performing the authorization check, you need to ensure that the user's organization is subscribed to a plan that has the `teams` feature. If the user's organization is not subscribed to a plan that has the `teams` feature, the authorization check will always return `false`, even if the user has the custom permission. diff --git a/docs/guides/multi-tenant-architecture.mdx b/docs/guides/multi-tenant-architecture.mdx index 6b604e09d2..e4384217f1 100644 --- a/docs/guides/multi-tenant-architecture.mdx +++ b/docs/guides/multi-tenant-architecture.mdx @@ -55,10 +55,10 @@ B2B SaaS applications with the following characteristics are well-supported with Clerk offers a number of building blocks to help integrate organizations into your application: - The [`` component](/docs/components/organization/organization-switcher) provides a way for your users to select which organization is active. The [`useOrganizationList()` hook](/docs/custom-flows/organization-switcher) can be used for more control. -- The [`useOrganization()` hook](/docs/hooks/use-organization) can be used to fetch the current, active organization. +- The [`useOrganization()` hook](/docs/hooks/use-organization) can be used to fetch the current [active organization](!active-organization). - The [`` component](/docs/components/protect) enables you to limit who can view certain pages based on their role. Additionally, Clerk exposes a number of helper functions, such as [`auth()`](/docs/references/nextjs/auth), and hooks, such as [`useAuth()`](/docs/hooks/use-auth), to check the user's authorization throughout your app and API endpoints. -The organization's ID should be stored in your database alongside each resource so that it can be used to filter and query the resources that should be rendered or returned according to the active organization. +The organization's ID should be stored in your database alongside each resource so that it can be used to filter and query the resources that should be rendered or returned according to the [active organization](!active-organization). ## Platforms diff --git a/docs/hooks/use-organization-list.mdx b/docs/hooks/use-organization-list.mdx index 15e26acd26..d5d24cb499 100644 --- a/docs/hooks/use-organization-list.mdx +++ b/docs/hooks/use-organization-list.mdx @@ -4,7 +4,7 @@ description: Access and manage the current user's organization list in your Reac sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start --- -The `useOrganizationList()` hook provides access to the current user's organization memberships, invitations, and suggestions. It also includes methods for creating new organizations and managing the active organization. +The `useOrganizationList()` hook provides access to the current user's organization memberships, invitations, and suggestions. It also includes methods for creating new organizations and managing the [active organization](!active-organization). ## Parameters diff --git a/docs/hooks/use-organization.mdx b/docs/hooks/use-organization.mdx index 4bf92725ed..f315877e46 100644 --- a/docs/hooks/use-organization.mdx +++ b/docs/hooks/use-organization.mdx @@ -6,7 +6,7 @@ search: sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start --- -The `useOrganization()` hook retrieves attributes of the currently active organization. +The `useOrganization()` hook retrieves attributes of the currently [active organization](!active-organization). ## Parameters diff --git a/docs/organizations/create-orgs-for-users.mdx b/docs/organizations/create-orgs-for-users.mdx index ccba43215c..320588b626 100644 --- a/docs/organizations/create-orgs-for-users.mdx +++ b/docs/organizations/create-orgs-for-users.mdx @@ -32,7 +32,7 @@ Although it may seem like a reasonable option, it's strongly recommended not to Let's walk through an example to see why. -Imagine you have an admin account configured to create organizations on a user's behalf during onboarding and then sends an invitation to the user to join the organization. When the user accepts the invitation, the organization will have their account plus the admin account in it. At that point, the organization has two monthly active users (MAUs), which makes it a **billable** organization. All [Clerk plans](https://clerk.com/pricing) come with 100 active organizations included for free, but over that limit, organizations are billed at $1 per month. In this case, for every user that is created in your app, they have an active organization automatically, because of the fact that the admin account is also in the organization. This tends to result in much higher costs than if users' organizations are created without an admin account included, since orgs with only one active user are not billable. Additionally, it's generally a nicer experience for users not to have extra admin accounts in their organizations. +Imagine you have an admin account configured to create organizations on a user's behalf during onboarding and then sends an invitation to the user to join the organization. When the user accepts the invitation, the organization will have their account plus the admin account in it. At that point, the organization has two monthly active users (MAUs), which makes it a **billable** organization. All [Clerk plans](https://clerk.com/pricing) come with 100 [active organizations](!active-organization) included for free, but over that limit, organizations are billed at $1 per month. In this case, for every user that is created in your app, they have an active organization automatically, because of the fact that the admin account is also in the organization. This tends to result in much higher costs than if users' organizations are created without an admin account included, since orgs with only one active user are not billable. Additionally, it's generally a nicer experience for users not to have extra admin accounts in their organizations. If you have an architecture scenario that isn't covered here or feel that it's critical to create organizations using an admin account, contact [support@clerk.com](mailto:support@clerk.com) for help. diff --git a/docs/organizations/force-organizations.mdx b/docs/organizations/force-organizations.mdx index b97b64e67d..4080c8f982 100644 --- a/docs/organizations/force-organizations.mdx +++ b/docs/organizations/force-organizations.mdx @@ -20,7 +20,7 @@ metadata: ]} /> -This guide demonstrates how to hide a user's personal account in order to appear as if they only have access to organizations, and how to limit access to your application to only users with active organizations, further enforcing organization-centric access. This is useful for applications that are built for organizations only, such as B2B applications. +This guide demonstrates how to hide a user's personal account in order to appear as if they only have access to organizations, and how to limit access to your application to only users with [active organizations](!active-organization), further enforcing organization-centric access. This is useful for applications that are built for organizations only, such as B2B applications. This guide is written for Next.js applications using App Router, but the same concepts can be applied to any application using Clerk. @@ -61,11 +61,11 @@ This guide is written for Next.js applications using App Router, but the same co ## Detect and set an active organization - A user can have many organization memberships, but only one of them can be active at a time. This is called the "active organization". + A user can have many organization memberships, but only one of them can be active at a time. This is called the [active organization](!active-organization). ### Detect an active organization - The [`Auth` object](/docs/references/backend/types/auth-object#auth-object-example-with-active-organization) includes information about the user's session, including the `orgId`. The `orgId` can be used to detect if a user has an active organization. To see how to access the `Auth` object using your preferred SDK, see the [reference doc](/docs/references/backend/types/auth-object#how-to-access-the-auth-object). The following examples use the Next.js SDK. + The [`Auth` object](/docs/references/backend/types/auth-object#auth-object-example-with-active-organization) includes information about the user's session, including the `orgId`. The `orgId` can be used to detect if a user has an [active organization](!active-organization). To see how to access the `Auth` object using your preferred SDK, see the [reference doc](/docs/references/backend/types/auth-object#how-to-access-the-auth-object). The following examples use the Next.js SDK. @@ -169,7 +169,7 @@ This guide is written for Next.js applications using App Router, but the same co ## Limit access to only users with active organizations - Now that you have hidden personal accounts from the UI and can detect and set an active organization, you can limit access to your application to users with active organizations only. This will ensure that users without active organizations cannot access your application. + Now that you have hidden personal accounts from the UI and can detect and set an [active organization](!active-organization), you can limit access to your application to users with active organizations only. This will ensure that users without active organizations cannot access your application. It's possible for a user to be signed in, but not have an active organization. This can happen in two cases: diff --git a/docs/organizations/org-slugs-in-urls.mdx b/docs/organizations/org-slugs-in-urls.mdx index 19ecee76d5..38ac7d641c 100644 --- a/docs/organizations/org-slugs-in-urls.mdx +++ b/docs/organizations/org-slugs-in-urls.mdx @@ -63,7 +63,7 @@ This guide shows you how to add organization slugs to your app's URLs, configure The following example uses the following URL structure: - - `/orgs/` indicates the **active organization**, followed by the **organization slug** + - `/orgs/` indicates the [**active organization**](!active-organization), followed by the **organization slug** - `/me/` indicates the **active personal account** | URL | What should be active? | What should be displayed? | @@ -131,7 +131,7 @@ This guide shows you how to add organization slugs to your app's URLs, configure ## Configure `clerkMiddleware()` to set the active organization > [!TIP] - > If your app doesn't use `clerkMiddleware()`, or you prefer to manually set the active organization, use the [`setActive()`](/docs/references/javascript/clerk) method to control the active organization on the client-side. See [this guide](/docs/organizations/force-organizations#set-an-active-organization-based-on-the-url) to learn how to manually activate a specific organization based on the URL. + > If your app doesn't use `clerkMiddleware()`, or you prefer to manually set the [active organization](!active-organization), use the [`setActive()`](/docs/references/javascript/clerk) method to control the active organization on the client-side. See [this guide](/docs/organizations/force-organizations#set-an-active-organization-based-on-the-url) to learn how to manually activate a specific organization based on the URL. With [`clerkMiddleware()`](/docs/references/nextjs/clerk-middleware), you can use the [`organizationSyncOptions`](/docs/references/nextjs/clerk-middleware#organization-sync-options) property to declare URL patterns that determine whether a specific organization or user's personal account should be activated. diff --git a/docs/references/astro/auth-store.mdx b/docs/references/astro/auth-store.mdx index 69e17f33d9..0169c79424 100644 --- a/docs/references/astro/auth-store.mdx +++ b/docs/references/astro/auth-store.mdx @@ -26,21 +26,21 @@ The `$authStore` store provides a convenient way to access the current auth stat - `orgId` - `string` - The ID of the user's active organization. + The ID of the user's [active organization](!active-organization). --- - `orgRole` - `string` - The current user's role in their active organization. + The current user's role in their [active organization](!active-organization). --- - `orgSlug` - `string` - The URL-friendly identifier of the user's active organization. + The URL-friendly identifier of the user's [active organization](!active-organization). ## How to use the `$authStore` store diff --git a/docs/references/astro/locals.mdx b/docs/references/astro/locals.mdx index 255a814eac..0f5234ec6c 100644 --- a/docs/references/astro/locals.mdx +++ b/docs/references/astro/locals.mdx @@ -8,7 +8,7 @@ Through Astro [`locals`](https://docs.astro.build/en/guides/middleware/#storing- ## `locals.auth()` -`Astro.locals.auth()` returns an `Auth` object. This JavaScript object contains important information like session data, your user's ID, as well as their active organization ID. Learn more about the `Auth` object [here](/docs/references/backend/types/auth-object){{ target: '_blank' }}. +`Astro.locals.auth()` returns an `Auth` object. This JavaScript object contains important information like session data, your user's ID, as well as the ID of the [active organization](!active-organization). Learn more about the `Auth` object [here](/docs/references/backend/types/auth-object){{ target: '_blank' }}. ### Example: Protect a page or form diff --git a/docs/references/astro/organization-store.mdx b/docs/references/astro/organization-store.mdx index 75ea58cb56..9dc4b91d08 100644 --- a/docs/references/astro/organization-store.mdx +++ b/docs/references/astro/organization-store.mdx @@ -8,7 +8,7 @@ The `$organizationStore` store is used to retrieve attributes of the currently a ## How to use the `$organizationStore` store -The following example demonstrates how to use the `$organizationStore` store to access the [`Organization`](/docs/references/javascript/organization){{ target: '_blank' }} object, which allows you to access the current active organization. +The following example demonstrates how to use the `$organizationStore` store to access the [`Organization`](/docs/references/javascript/organization){{ target: '_blank' }} object, which allows you to access the current [active organization](!active-organization). ```tsx {{ filename: 'organization.tsx' }} diff --git a/docs/references/backend/types/auth-object.mdx b/docs/references/backend/types/auth-object.mdx index be5f3ce849..9de6574c65 100644 --- a/docs/references/backend/types/auth-object.mdx +++ b/docs/references/backend/types/auth-object.mdx @@ -36,28 +36,28 @@ The `Auth` object contains important information like the current user's session - `orgId` - `string | undefined` - The ID of the user's active organization. + The ID of the user's [active organization](!active-organization). --- - `orgRole` - [OrganizationCustomRoleKey](/docs/references/javascript/types/organization-custom-role-key) | undefined - The current user's role in their active organization. + The current user's role in their [active organization](!active-organization). --- - `orgSlug` - `string | undefined` - The URL-friendly identifier of the user's active organization. + The URL-friendly identifier of the user's [active organization](!active-organization). --- - `orgPermissions` - [OrganizationCustomPermissionKey](/docs/references/javascript/types/organization-custom-permission-key)\[] | undefined - The current user's active organization permissions. + The current user's [active organization](!active-organization) permissions. --- @@ -342,7 +342,7 @@ The `Auth` object is not available in the frontend. To use the `getToken()` meth ## `Auth` object example without active organization -The following is an example of the `Auth` object without an active organization. Notice that there is no `o` claim. Read more about token claims in the [guide on session tokens](/docs/backend-requests/resources/session-tokens). +The following is an example of the `Auth` object without an [active organization](!active-organization). Notice that there is no `o` claim. Read more about token claims in the [guide on session tokens](/docs/backend-requests/resources/session-tokens). @@ -398,7 +398,7 @@ The following is an example of the `Auth` object without an active organization. ## `Auth` object example with active organization -The following is an example of the `Auth` object with an active organization. Notice the addition of the `o` claim. Read more about token claims in the [guide on session tokens](/docs/backend-requests/resources/session-tokens). +The following is an example of the `Auth` object with an [active organization](!active-organization). Notice the addition of the `o` claim. Read more about token claims in the [guide on session tokens](/docs/backend-requests/resources/session-tokens). diff --git a/docs/references/javascript/clerk.mdx b/docs/references/javascript/clerk.mdx index 24d93a2085..3119e6a98b 100644 --- a/docs/references/javascript/clerk.mdx +++ b/docs/references/javascript/clerk.mdx @@ -530,7 +530,7 @@ function getOrganization(organizationId: string): Promise [!WARNING] > You must have [**Verified domains**][verified-domains-ref] enabled in your app's settings in the Clerk Dashboard. @@ -198,7 +198,7 @@ await clerk.organization.getDomain({ domainId: 'domain_123' }) ### `getDomains()` -Retrieves the list of domains for the currently active organization. Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationDomain`][org-domain-ref] objects. +Retrieves the list of domains for the currently [active organization](!active-organization). Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationDomain`][org-domain-ref] objects. > [!WARNING] > You must have [**Verified domains**][verified-domains-ref] enabled in your app's settings in the Clerk Dashboard. @@ -238,7 +238,7 @@ await clerk.organization.getDomains() ### `getInvitations()` -Retrieves the list of invitations for the currently active organization. Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationInvitation`][org-inv-ref] objects. +Retrieves the list of invitations for the currently [active organization](!active-organization). Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationInvitation`][org-inv-ref] objects. ```typescript function getInvitations( @@ -277,7 +277,7 @@ await clerk.organization.getInvitations() ### `getMemberships()` -Retrieves the list of memberships for the currently active organization. Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationMembership`][org-mem-ref] objects. +Retrieves the list of memberships for the currently [active organization](!active-organization). Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationMembership`][org-mem-ref] objects. ```typescript function getMemberships( @@ -314,7 +314,7 @@ For an example on how to use `getMemberships()`, see the [custom flow on managin ### `getMembershipRequests()` -Retrieve the list of membership requests for the currently active organization. Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationMembershipRequest`][org-mem-ref]-request) objects. +Retrieve the list of membership requests for the currently [active organization](!active-organization). Returns a [`ClerkPaginatedResponse`][pag-ref] of [`OrganizationMembershipRequest`][org-mem-ref]-request) objects. > [!WARNING] > You must have [**Organizations**](/docs/organizations/overview#enable-organizations-in-your-application), and [**Verified domains** and **Automatic suggestion**][verified-domains-ref] enabled in your app's settings in the Clerk Dashboard. diff --git a/docs/references/javascript/overview.mdx b/docs/references/javascript/overview.mdx index d60a5bb30a..b9eadc85ae 100644 --- a/docs/references/javascript/overview.mdx +++ b/docs/references/javascript/overview.mdx @@ -38,4 +38,4 @@ The [`SignUp`](/docs/references/javascript/sign-up) object holds the state of th ### `Organization` -Organizations are a flexible and scalable way to manage users and their access to resources within your Clerk application. With organizations, you can assign specific roles and permissions to users, making them useful for managing projects, coordinating teams, or facilitating partnerships. Users can belong to many organizations. One of them will be the ["active organization"](/docs/organizations/overview#active-organization) of the session. It is represented by the [`Organization`](/docs/references/javascript/organization) object. To learn about organizations, see the [dedicated guide](/docs/organizations/overview). +Organizations are a flexible and scalable way to manage users and their access to resources within your Clerk application. With organizations, you can assign specific roles and permissions to users, making them useful for managing projects, coordinating teams, or facilitating partnerships. Users can belong to many organizations. One of them will be the [active organization](!active-organization) of the session. It is represented by the [`Organization`](/docs/references/javascript/organization) object. To learn about organizations, see the [dedicated guide](/docs/organizations/overview). diff --git a/docs/references/javascript/session.mdx b/docs/references/javascript/session.mdx index ad2be987c1..c7a66858a0 100644 --- a/docs/references/javascript/session.mdx +++ b/docs/references/javascript/session.mdx @@ -93,7 +93,7 @@ All sessions that are **expired**, **removed**, **replaced**, **ended** or **aba - `lastActiveOrganizationId` - `string | null` - The ID of the last active organization. + The ID of the last [active organization](!active-organization). --- @@ -322,7 +322,7 @@ function getToken(options?: GetTokenOptions): Promise - `organizationId?` - `string` - The organization associated with the generated session token. _Does not modify the session's currently active organization._ + The organization associated with the generated session token. _Does not modify the session's currently [active organization](!active-organization)._ #### Example diff --git a/docs/references/javascript/types/set-active-params.mdx b/docs/references/javascript/types/set-active-params.mdx index 8254edfcb6..df86285358 100644 --- a/docs/references/javascript/types/set-active-params.mdx +++ b/docs/references/javascript/types/set-active-params.mdx @@ -17,7 +17,7 @@ The parameters for the `setActive()` method. - `organization` - [Organization](/docs/references/javascript/organization) | string | null - The organization resource or organization ID/slug (string version) to be set as active in the current session. If `null`, the currently active organization is removed as active. + The organization resource or organization ID/slug (string version) to be set as active in the current session. If `null`, the currently [active organization](!active-organization) is removed as active. --- diff --git a/docs/references/nextjs/clerk-middleware.mdx b/docs/references/nextjs/clerk-middleware.mdx index e922519f0e..c7a63ad7b5 100644 --- a/docs/references/nextjs/clerk-middleware.mdx +++ b/docs/references/nextjs/clerk-middleware.mdx @@ -417,7 +417,7 @@ object has the type `OrganizationSyncOptions`, which has the following propertie Patterns must have a path parameter named either `:id` (to match a Clerk organization ID) or `:slug` (to match a Clerk organization slug). > [!WARNING] - > If the organization can't be activated—either because it doesn't exist or the user lacks access—the previously active organization will remain unchanged. Components must detect this case and provide an appropriate error and/or resolution pathway, such as calling `notFound()` or displaying an [``](/docs/components/organization/organization-switcher). + > If the organization can't be activated—either because it doesn't exist or the user lacks access—the previously [active organization](!active-organization) will remain unchanged. Components must detect this case and provide an appropriate error and/or resolution pathway, such as calling `notFound()` or displaying an [``](/docs/components/organization/organization-switcher). Common examples: diff --git a/docs/references/sdk/terminology.mdx b/docs/references/sdk/terminology.mdx index 8ddee067eb..2dfc09297c 100644 --- a/docs/references/sdk/terminology.mdx +++ b/docs/references/sdk/terminology.mdx @@ -11,7 +11,7 @@ A consistent terminology should be used across all user interactions with Clerk' | Client | A [client](/docs/references/javascript/client){{ target: '_blank' }} represents the current device or software accessing an application such as your web browser, native application, Chrome Extension, or Electron app. | | Session | A [session](/docs/references/javascript/session){{ target: '_blank' }} is a secure representation of the authentication state of the current user. Each client can hold multiple sessions on the same device. This is identical to how Gmail works in a browser. | | User | A user represents the current user of the session. The [`User`](/docs/references/javascript/user){{ target: '_blank' }} object holds all the basic user information e.g. name, email addresses, phone numbers, etc… including their public, private, and unsafe metadata. | -| Organization | An [organization](/docs/references/javascript/organization){{ target: '_blank' }} represents the current organization of the session. Users can belong to many organizations. One of them will be the [active organization](/docs/organizations/overview#active-organization) of the session. | +| Organization | An [organization](/docs/references/javascript/organization){{ target: '_blank' }} represents the current organization of the session. Users can belong to many organizations. One of them will be the [active organization](!active-organization) of the session. | | FAPI | [Frontend API of Clerk](/docs/reference/frontend-api){{ target: '_blank' }}. Example: `https://random-name.clerk.accounts.dev` (Production example: `https://clerk.yourdomain.com`). FAPI is the primary API for Clerk’s UI components. Every Clerk development/production instance has a dedicated FAPI. This is the authentication, session, user & organization management API you or your users will interact with. | | BAPI | [Backend API of Clerk](/docs/reference/backend-api){{ target: '_blank' }}. Currently set to `https://api.clerk.com`. A restful CRUD API for the server-side. | | Secret Key | Your app’s Secret Key for use in the backend. Do not expose this on the frontend with a public environment variable. Allows for CRUD operations. | diff --git a/docs/references/vue/use-auth.mdx b/docs/references/vue/use-auth.mdx index 194c544f2b..1dd205b548 100644 --- a/docs/references/vue/use-auth.mdx +++ b/docs/references/vue/use-auth.mdx @@ -40,21 +40,21 @@ The `useAuth()` composable provides access to the current user's authentication - `orgId` - `Ref` - The ID of the user's active organization. + The ID of the user's [active organization](!active-organization). --- - `orgRole` - `Ref` - The current user's role in their active organization. + The current user's role in their [active organization](!active-organization). --- - `orgSlug` - `Ref` - The URL-friendly identifier of the user's active organization. + The URL-friendly identifier of the user's [active organization](!active-organization). --- diff --git a/docs/references/vue/use-organization.mdx b/docs/references/vue/use-organization.mdx index e8513b2763..53304c8926 100644 --- a/docs/references/vue/use-organization.mdx +++ b/docs/references/vue/use-organization.mdx @@ -4,7 +4,7 @@ description: Access and manage the currently active organization in your Vue app sdk: vue --- -The `useOrganization()` composable retrieves attributes of the currently active organization. +The `useOrganization()` composable retrieves attributes of the currently [active organization](!active-organization). ## Returns @@ -19,7 +19,7 @@ The `useOrganization()` composable retrieves attributes of the currently active - `organization` - Ref\<[Organization](/docs/references/javascript/organization)> - The currently active organization. + The currently [active organization](!active-organization). --- diff --git a/docs/upgrade-guides/core-2/chrome-extension.mdx b/docs/upgrade-guides/core-2/chrome-extension.mdx index ebdab12588..add6c58b36 100644 --- a/docs/upgrade-guides/core-2/chrome-extension.mdx +++ b/docs/upgrade-guides/core-2/chrome-extension.mdx @@ -512,7 +512,7 @@ As part of this major version, a number of previously deprecated props, argument - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -525,7 +525,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/docs/upgrade-guides/core-2/expo.mdx b/docs/upgrade-guides/core-2/expo.mdx index 357079f200..e1a3e70201 100644 --- a/docs/upgrade-guides/core-2/expo.mdx +++ b/docs/upgrade-guides/core-2/expo.mdx @@ -504,7 +504,7 @@ As part of this major version, a number of previously deprecated props, argument - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -517,7 +517,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/docs/upgrade-guides/core-2/javascript.mdx b/docs/upgrade-guides/core-2/javascript.mdx index 0798bc3967..2d3e430748 100644 --- a/docs/upgrade-guides/core-2/javascript.mdx +++ b/docs/upgrade-guides/core-2/javascript.mdx @@ -563,7 +563,7 @@ As part of this major version, a number of previously deprecated props, argument titles={["setSession -> setActive", "Organization.create('x') -> Organization.create({ name: 'x' })", "Organization.getPendingInvitations() -> Organization.getInvitations({ status: 'pending' })", "MagicLinkError -> EmailLinkError", "isMagicLinkError -> isEmailLinkError", "MagicLinkErrorCode -> EmailLinkErrorCode", "useMagicLink -> useEmailLink", "handleMagicLinkVerification -> handleEmailLinkVerification", "[User|OrganizationMembershipPublicData].profileImageUrl -> [User|OrganizationMembershipPublicData].imageUrl", "Clerk.getOrganizationMemberships() -> user.getOrganizationMemberships()", "lastOrganizationInvitation and lastOrganizationMember dropped from event emitter", "Clerk.redirectToHome() removed", "Clerk.isReady() removed", "Replace signOutCallback prop on SignOutButton with redirectUrl", "Clerk import changed"]} > - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -576,7 +576,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/docs/upgrade-guides/core-2/nextjs.mdx b/docs/upgrade-guides/core-2/nextjs.mdx index 8a67ee240c..443154a712 100644 --- a/docs/upgrade-guides/core-2/nextjs.mdx +++ b/docs/upgrade-guides/core-2/nextjs.mdx @@ -1045,7 +1045,7 @@ As part of this major version, a number of previously deprecated props, argument - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -1058,7 +1058,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/docs/upgrade-guides/core-2/react.mdx b/docs/upgrade-guides/core-2/react.mdx index 33891d8d89..344f5a6bd6 100644 --- a/docs/upgrade-guides/core-2/react.mdx +++ b/docs/upgrade-guides/core-2/react.mdx @@ -783,7 +783,7 @@ As part of this major version, a number of previously deprecated props, argument - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -796,7 +796,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/docs/upgrade-guides/core-2/remix.mdx b/docs/upgrade-guides/core-2/remix.mdx index 3c1d7d62cc..af42d51dbb 100644 --- a/docs/upgrade-guides/core-2/remix.mdx +++ b/docs/upgrade-guides/core-2/remix.mdx @@ -340,7 +340,7 @@ As part of this major version, a number of previously deprecated props, argument titles={["setSession -> setActive", "Organization.create('x') -> Organization.create({ name: 'x' })", "Organization.getPendingInvitations() -> Organization.getInvitations({ status: 'pending' })", "isMagicLinkError -> isEmailLinkError", "MagicLinkErrorCode -> EmailLinkErrorCode", "useMagicLink -> useEmailLink", "Replace signOutCallback prop on SignOutButton with redirectUrl"]} > - `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently active organization. The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: + `setSession` should be replaced with `setActive`. The format of the parameters has changed slightly - `setActive` takes an object where `setSession` took params directly. The `setActive` function also can accept an `organization` param that is used to set the currently [active organization](!active-organization). The return signature did not change. Read the [API documentation](/docs/references/javascript/clerk#set-active) for more detail. This function should be expected to be returned from one of the following Clerk hooks: `useSessionList`, `useSignUp`, or `useSignIn`. Some migration examples: ```js {{ prettier: false, del: [1, 4, 7], ins: [2, 5, 8] }} await setSession('sessionID', () => void) @@ -353,7 +353,7 @@ As part of this major version, a number of previously deprecated props, argument await setActive({ session: sessionObj, beforeEmit: () => void }) ``` - `setActive` also supports setting an active organization: + `setActive` also supports setting an [active organization](!active-organization): ```js {{ prettier: false }} await setActive({ diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index d8f0a0a8bd..ae4cc187e0 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -5275,3 +5275,50 @@ Updated Documentation specific to React.js `) }) }) + +describe('Test tooltips', () => { + test('Should embed tooltips into a doc', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], + }), + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: x +--- + +[Tooltip](!ABC) +`, + }, + { + path: './docs/_tooltips/ABC.mdx', + content: `React.js is a framework or a library idk`, + }, + ]) + + await build( + await createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + tooltips: { + inputPath: '../docs/_tooltips', + outputPath: './_tooltips', + }, + }), + ) + + expect(await readFile('./dist/api-doc.mdx')).toBe(`--- +title: API Documentation +description: x +--- + +TooltipReact.js is a framework or a library idk +`) + }) +}) diff --git a/scripts/lib/plugins/checkTooltips.ts b/scripts/lib/plugins/checkTooltips.ts index 3a81dc63b9..8e06362865 100644 --- a/scripts/lib/plugins/checkTooltips.ts +++ b/scripts/lib/plugins/checkTooltips.ts @@ -10,9 +10,7 @@ import type { VFile } from 'vfile' import type { BuildConfig } from '../config' import { safeMessage } from '../error-messages' import type { DocsFile } from '../io' -import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' import { removeMdxSuffix } from '../utils/removeMdxSuffix' -import { z } from 'zod' import { u as mdastBuilder } from 'unist-builder' export const checkTooltips = @@ -32,28 +30,28 @@ export const checkTooltips = () => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { - const tooltipSrc = extractComponentPropValueFromNode( - config, - node, - vfile, - 'Tooltip', - 'src', - true, - 'docs', - file.filePath, - z.string(), - ) + // Tooltips are written as links with the format [trigger](!content) + // We need to check if the node is a link + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (!('children' in node)) return node + if (typeof node.url !== 'string') return node - if (tooltipSrc === undefined) return node + // Then, check if the link is a tooltip (starts with !) + // [trigger text](!content) + if (!node.url.startsWith('!')) return node - if (tooltipSrc.startsWith('_tooltips/') === false) { - if (options.reportWarnings === true) { - safeMessage(config, vfile, file.filePath, 'docs', 'tooltip-src-not-tooltip', [], node.position) - } - return node - } + // Access the link properties + // url of the link = e.g. '!content' + // children (the text content of the link) = e.g. 'trigger text' + const url = node.url + const children = node.children + + // The tooltip content exists in a MDX file e.g. '_tooltips/content.mdx' + // We need to remove the ! to get the file path e.g. 'content' + const tooltipSrc = url.substring(1) - const tooltip = tooltips.find((tooltip) => tooltip.path === `${removeMdxSuffix(tooltipSrc)}.mdx`) + const tooltip = tooltips.find((tooltip) => tooltip.path === `_tooltips/${removeMdxSuffix(tooltipSrc)}.mdx`) if (tooltip === undefined) { if (options.reportWarnings === true) { @@ -81,7 +79,7 @@ export const checkTooltips = children: [ mdastBuilder('mdxJsxTextElement', { name: 'TooltipTrigger', - children: (node as any).children, + children: children, }), mdastBuilder('mdxJsxTextElement', { name: 'TooltipContent', diff --git a/styleguides/STYLEGUIDE.md b/styleguides/STYLEGUIDE.md index 7f74859495..27bb96b5ab 100644 --- a/styleguides/STYLEGUIDE.md +++ b/styleguides/STYLEGUIDE.md @@ -8,7 +8,7 @@ These are the guidelines we use to write our docs. Try to keep things in alphabetic order, except Next.js, React, and JavaScript are prioritized as these are our core SDKs. For example, our SDK selector prioritizes Next.js, React, and Javascript, and then alphabetizes the rest of the SDK's. Another example is that whenever there is a `` component, the `items` should follow this same rule. -### De-dupe reference links +### De-dupe reference links and tooltips When mentioning a documented component, function, etc, multiple times on a page, link to the reference documentation on the **first mention** of that item. The exception to this rule is when the reference is mentioned under a different heading. In that case, link to the reference documentation again. @@ -18,6 +18,8 @@ When mentioning a documented component, function, etc, multiple times on a page, > ✅ > The [`currentUser()`](https://clerk.com/docs/references/nextjs/current-user) helper will return the [`User`](https://clerk.com/docs/references/javascript/user) object of the currently active user. The following example uses the `currentUser()` helper to access the `User` object for the authenticated user. +This same rule applies to tooltips. + ### Use sentence-case for titles > ❌ From b351accdba3fbaa295f078695fe9adb0f188ee99 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 17 Sep 2025 18:36:28 +0800 Subject: [PATCH 10/11] Add test to check that links in tooltips are validated --- scripts/build-docs.test.ts | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index b9c213825f..664c6b0f9b 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -5497,4 +5497,45 @@ canonical: /docs/api-doc TooltipReact.js is a framework or a library idk `) }) + + test('Should validate links in tooltips', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], + }), + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: x +--- + +[Tooltip](!ABC) +`, + }, + { + path: './docs/_tooltips/ABC.mdx', + content: `This is an invalid link [Invalid Link](/docs/invalid-link)`, + }, + ]) + + const output = await build( + await createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + tooltips: { + inputPath: '../docs/_tooltips', + outputPath: './_tooltips', + }, + }), + ) + + expect(output).toContain( + 'warning Matching file not found for path: /docs/invalid-link. Expected file to exist at /docs/invalid-link.mdx', + ) + }) }) From f6ea121fbe182deb729768468088fecd0ea6b8e0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 17 Sep 2025 18:37:25 +0800 Subject: [PATCH 11/11] Remove un-used error message --- scripts/lib/error-messages.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts index aa3e807509..a2ea951e02 100644 --- a/scripts/lib/error-messages.ts +++ b/scripts/lib/error-messages.ts @@ -77,7 +77,6 @@ export const errorMessages = { // Tooltip errors 'tooltip-read-error': (path: string): string => `Failed to read in ${path} from tooltips file`, 'tooltip-parse-error': (path: string): string => `Failed to parse the content of ${path}`, - 'tooltip-src-not-tooltip': (): string => ` prop "src" must start with "_tooltips/"`, 'tooltip-not-found': (src: string): string => `Tooltip ${src} not found`, // Typedoc errors