From 73f61be1e581d79e085dc8665ea2ed2d7733e4ee Mon Sep 17 00:00:00 2001 From: Michael Perrotte Date: Tue, 15 Sep 2026 10:26:43 -0400 Subject: [PATCH 1/4] feat(docs): add Mintlify SDK reference pipeline and TypeDoc plugins - Refactor typedoc.js to use entryPoints resolve strategy and auth0 theme - Add scripts/typedoc-plugin.js: categorizes exports and extends sidebar nav - Add scripts/typedoc-plugin-mintlify.js: injects markdown type links for Mintlify - Add typedoc.docsv2.js: JSON-only config for docs-v2 Mintlify pipeline - Add docs:docsv2 npm script for generating the Mintlify artifact --- package.json | 11 +- scripts/typedoc-plugin-mintlify.js | 141 ++++++++++ scripts/typedoc-plugin.js | 425 +++++++++++++++++++++++++++++ typedoc.docsv2.js | 41 +++ typedoc.js | 73 ++++- 5 files changed, 681 insertions(+), 10 deletions(-) create mode 100644 scripts/typedoc-plugin-mintlify.js create mode 100644 scripts/typedoc-plugin.js create mode 100644 typedoc.docsv2.js diff --git a/package.json b/package.json index ad00e544..e4047413 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "test:dist": "npm run build && npm run test:dist:only", "test:dist:only": "jest use-client-directive", "prepack": "npm run build", - "docs": "typedoc --options typedoc.js src", + "docs": "typedoc --options ./typedoc.js", + "docs:docsv2": "typedoc --options ./typedoc.docsv2.js", "install:examples": "npm i --prefix=examples/vite-react-router --no-package-lock --legacy-peer-deps && npm i --prefix=examples/gatsby-app --no-package-lock --legacy-peer-deps && npm i --prefix=examples/nextjs-app --no-package-lock --legacy-peer-deps && npm ci --prefix=examples/users-api", "start:vite": "npm start --prefix=examples/vite-react-router", "start:gatsby": "npm start --prefix=examples/gatsby-app", @@ -60,8 +61,8 @@ "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.3", "@types/jest": "^29.5.14", - "@types/react": "19.3.0", - "@types/react-dom": "19.3.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.7", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "cypress": "^15.18.0", @@ -75,8 +76,8 @@ "oidc-provider": "^8.8.1", "prettier": "^2.8.1", "pretty-quick": "^3.1.3", - "react": "19.3.0", - "react-dom": "19.3.0", + "react": "19.2.8", + "react-dom": "19.2.8", "rollup": "^3.7.0", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-delete": "^2.0.0", diff --git a/scripts/typedoc-plugin-mintlify.js b/scripts/typedoc-plugin-mintlify.js new file mode 100644 index 00000000..951b0ca4 --- /dev/null +++ b/scripts/typedoc-plugin-mintlify.js @@ -0,0 +1,141 @@ +// @ts-check +/** + * TypeDoc plugin for the Mintlify SDK reference pipeline. + * + * Mintlify renders a type reference as a plain text pill, so `mfa: MfaApiClient` + * on the `Auth0Client` page is a dead end even though `MfaApiClient` has its own + * generated page. Mintlify's renderer reads only a symbol's comment summary: + * `@see` block tags are dropped and `{@link}` inline tags are resolved by + * TypeDoc before serialization, so the one thing that survives into the page is + * markdown in the summary itself. + * + * This plugin therefore appends a "Type:" line of markdown links to every + * symbol whose type mentions another documented export. It runs only for the + * JSON artifact, never for the HTML site, where TypeDoc already links types. + */ +const { Converter, ParameterType, ReflectionKind, Type } = require('typedoc'); + +/** + * URL segment Mintlify assigns to each page-level `ReflectionKind`. These are + * not documented anywhere; they were read off a generated build. Note they + * differ from TypeDoc's own HTML theme, which uses `enumerations/` and + * `type-aliases/`. + */ +const KIND_SEGMENT = { + [ReflectionKind.Enum]: 'enums', + [ReflectionKind.Variable]: 'variables', + [ReflectionKind.Function]: 'functions', + [ReflectionKind.Class]: 'classes', + [ReflectionKind.Interface]: 'interfaces', + [ReflectionKind.TypeAlias]: 'types' +}; + +/** Kinds whose pages carry the type pills worth linking. */ +const LINKABLE_KINDS = + ReflectionKind.Property | + ReflectionKind.Accessor | + ReflectionKind.Parameter | + ReflectionKind.TypeAlias | + ReflectionKind.Variable; + +/** + * Collect the names of every reference type mentioned anywhere in a type, + * including inside type arguments, unions, intersections and arrays. A + * `Promise` return type should link the + * response, not be skipped for being wrapped. + * + * Recursion is restricted to `Type` instances on purpose. Some types hold a + * `declaration` pointing back at a reflection, and following that walks into + * the whole project graph and overflows the stack. + * + * @param {import('typedoc').SomeType | undefined} type + * @param {Set} out + */ +function collectReferenceNames(type, out) { + if (!(type instanceof Type)) return; + + if (type.type === 'reference' && typeof type.name === 'string') { + out.add(type.name); + } + + for (const value of Object.values(type)) { + if (Array.isArray(value)) { + value.forEach(entry => collectReferenceNames(entry, out)); + } else { + collectReferenceNames(/** @type {any} */ (value), out); + } + } +} + +/** @param {import('typedoc').Application} app */ +function load(app) { + app.options.addDeclaration({ + name: 'mintlifyDirectory', + help: 'URL prefix Mintlify serves the generated SDK pages under. Must match `sdk.directory` in docs.json.', + type: ParameterType.String, + defaultValue: 'sdk/typescript' + }); + + // Priority -1000 so this runs after the category plugin and after TypeDoc's + // own resolution: by now every type reference has a resolved name. + app.converter.on( + Converter.EVENT_RESOLVE_END, + context => { + const { project } = context; + const directory = String( + app.options.getValue('mintlifyDirectory') + ).replace(/^\/|\/$/g, ''); + + /** Documented top-level exports, by name, that Mintlify gives a page. */ + const pages = new Map(); + for (const child of project.children ?? []) { + const segment = KIND_SEGMENT[child.kind]; + if (segment) { + pages.set(child.name, `/${directory}/${segment}/${child.name}`); + } + } + + for (const reflection of project.getReflectionsByKind(LINKABLE_KINDS)) { + const names = new Set(); + collectReferenceNames(/** @type {any} */ (reflection).type, names); + + // A symbol never needs a link to its own page. + names.delete(reflection.name); + names.delete(reflection.parent?.name ?? ''); + + const links = [...names] + .filter(name => pages.has(name)) + .sort() + .map(name => `[${name}](${pages.get(name)})`); + + if (!links.length) continue; + + appendSummary(reflection, `Type: ${links.join(', ')}`); + } + }, + undefined, + -1000 + ); +} + +/** + * Append a markdown paragraph to a reflection's comment summary, creating the + * comment if the symbol is undocumented. Mintlify renders the summary as MDX, + * so the markdown becomes real anchors. + * + * @param {import('typedoc').Reflection} reflection + * @param {string} markdown + */ +function appendSummary(reflection, markdown) { + const { Comment } = require('typedoc'); + const target = /** @type {any} */ (reflection); + const comment = target.comment ?? target.signatures?.[0]?.comment; + + if (comment) { + comment.summary.push({ kind: 'text', text: `\n\n${markdown}` }); + } else { + target.comment = new Comment([{ kind: 'text', text: markdown }]); + } +} + +module.exports = { load, KIND_SEGMENT }; diff --git a/scripts/typedoc-plugin.js b/scripts/typedoc-plugin.js new file mode 100644 index 00000000..10aa794e --- /dev/null +++ b/scripts/typedoc-plugin.js @@ -0,0 +1,425 @@ +// @ts-check +/** + * TypeDoc plugin that shapes the generated API reference for readability. + * + * Two jobs: + * + * 1. Categorize every top-level export so the landing page and sidebar read as + * "Getting Started / Hooks & HOCs / Context / Errors / ..." instead of one + * flat alphabetical list of ~110 symbols. Categories are derived from the + * export's name and source path, so new exports get sorted automatically + * without anyone having to add an `@category` tag by hand. A tag written in + * the source always wins. + * + * 2. Put the context interfaces' members directly in the sidebar, so + * `getAccessTokenSilently` or `loginWithRedirect` is one click from anywhere + * rather than "click the interface, then scan an index, then click again". + * The default theme stops the navigation tree at module level, so we extend + * DefaultTheme to add members for the entry-point interfaces only. + */ +const { + Comment, + CommentTag, + Converter, + DefaultTheme, + JSX, + ReflectionKind +} = require('typedoc'); + +/** + * Interfaces that are the SDK's real entry points: everything a component gets + * back from `useAuth0()`. Their members go in the sidebar. + */ +const ENTRY_INTERFACES = ['Auth0ContextInterface']; + +const SETUP = 'Getting Started'; +const HOOKS = 'Hooks & HOCs'; +const CONTEXT = 'Context'; +const CONFIGURATION = 'Configuration'; +const AUTHENTICATION = 'Login & Logout'; +const TOKENS = 'Tokens & Users'; +const MFA = 'Multi-Factor Authentication'; +const PASSKEYS = 'Passkeys'; +const MY_ACCOUNT = 'My Account'; +const ERRORS = 'Errors'; +const CACHING = 'Caching'; +const OTHER = 'Other Types'; + +/** + * Category order on the landing page and in the sidebar. `*` is where any + * category not listed here lands. + */ +const CATEGORY_ORDER = [ + SETUP, + HOOKS, + CONTEXT, + CONFIGURATION, + AUTHENTICATION, + TOKENS, + MFA, + PASSKEYS, + MY_ACCOUNT, + ERRORS, + CACHING, + '*', + OTHER +]; + +/** The provider and the props you hand it: the first thing anyone reads. */ +const SETUP_EXPORTS = new Set([ + 'Auth0Provider', + 'Auth0ProviderOptions', + 'Auth0ProviderWithConfigOptions', + 'Auth0ProviderWithClientOptions', + 'AppState' +]); + +/** The consumption surface: hooks and higher-order components. */ +const HOOKS_EXPORTS = new Set([ + 'useAuth0', + 'useAuth0Suspense', + 'withAuth0', + 'WithAuth0Props', + 'withAuthenticationRequired', + 'WithAuthenticationRequiredOptions' +]); + +/** + * The context object and the shapes it carries. `initialContext` is exported but + * carries `@ignore`, so it never reaches the reference. + */ +const CONTEXT_EXPORTS = new Set([ + 'Auth0Context', + 'Auth0ContextInterface', + 'Auth0SuspenseContextInterface' +]); + +/** Exports that belong in "Configuration" regardless of kind. */ +const CONFIGURATION_EXPORTS = new Set([ + 'AuthorizationParams', + 'ClientConfiguration', + 'CacheLocation', + 'RefreshTokenMode', + 'ResponseType', + 'InteractiveErrorHandler' +]); + +/** Options and results for the login, logout and connect-account flows. */ +const AUTHENTICATION_EXPORTS = new Set([ + 'RedirectLoginOptions', + 'RedirectLoginResult', + 'PopupLoginOptions', + 'PopupConfigOptions', + 'LogoutOptions', + 'LogoutUrlOptions', + 'RedirectConnectAccountOptions', + 'ConnectAccountRedirectResult', + 'ConnectedAccount' +]); + +/** Everything about acquiring tokens and reading the resulting identity. */ +const TOKEN_EXPORTS = new Set([ + 'GetTokenSilentlyOptions', + 'GetTokenWithPopupOptions', + 'TokenEndpointResponse', + 'RevokeRefreshTokenOptions', + 'CustomTokenExchangeOptions', + 'User', + 'IdToken', + 'ActClaim', + 'FetcherConfig' +]); + +/** Cache implementations and the interface they satisfy. */ +const CACHING_EXPORTS = new Set([ + 'ICache', + 'InMemoryCache', + 'LocalStorageCache', + 'Cacheable' +]); + +/** Names in "My Account" that carry no `MyAccount` prefix to match on. */ +const MY_ACCOUNT_EXPORTS = new Set([ + 'AuthenticationMethod', + 'AuthenticationMethodType', + 'Factor', + 'UpdateAuthenticationMethodRequest', + 'EnrollmentChallengeOptions', + 'EnrollmentChallengeResponse', + 'EnrollmentVerifyOptions' +]); + +/** + * Decide which category a top-level export belongs to. Driven by name and + * source path so that new exports land somewhere sensible on their own. + * + * @param {import('typedoc').DeclarationReflection} reflection + * @returns {string} + */ +function categoryFor(reflection) { + const { name } = reflection; + + if (SETUP_EXPORTS.has(name)) return SETUP; + if (HOOKS_EXPORTS.has(name)) return HOOKS; + if (CONTEXT_EXPORTS.has(name)) return CONTEXT; + + // Errors first: an error's home is the Errors section even when a feature + // prefix below would otherwise claim it (MfaVerifyError, PasskeyError, ...). + if (/Error$/.test(name) || name === 'MfaRequirements') return ERRORS; + + if (CONFIGURATION_EXPORTS.has(name)) return CONFIGURATION; + if (AUTHENTICATION_EXPORTS.has(name)) return AUTHENTICATION; + if (TOKEN_EXPORTS.has(name)) return TOKENS; + if (CACHING_EXPORTS.has(name)) return CACHING; + if (MY_ACCOUNT_EXPORTS.has(name)) return MY_ACCOUNT; + + // Nearly everything else is re-exported from `@auth0/auth0-spa-js`, so there + // is no path under `src/` to match on: the name is all we have. + if (name.startsWith('MyAccount')) return MY_ACCOUNT; + if (name.startsWith('Passkey')) return PASSKEYS; + if (name.startsWith('Mfa') || name.startsWith('Enroll')) return MFA; + if ( + name === 'Authenticator' || + name === 'ChallengeAuthenticatorParams' || + name === 'ChallengeResponse' || + name === 'VerifyParams' + ) { + return MFA; + } + + // Source paths are relative to TypeDoc's computed base path, which shifts + // depending on which files end up in the program, so match on the directory + // segment rather than a prefix. + const fileName = reflection.sources?.[0]?.fileName ?? ''; + const inDir = dir => fileName.includes(`${dir}/`); + + if (inDir('mfa')) return MFA; + if (inDir('passkey')) return PASSKEYS; + if (inDir('myaccount')) return MY_ACCOUNT; + if (inDir('cache')) return CACHING; + + return OTHER; +} + +/** + * Categories for `Auth0ContextInterface`'s own members, so its page groups 25+ + * entries by task instead of listing them all under one "Properties" heading. + * Anything not listed here falls into "Advanced". + */ +const CONTEXT_MEMBER_CATEGORIES = { + 'Auth State': ['isLoading', 'isAuthenticated', 'user', 'error'], + 'Sub-clients': ['mfa', 'passkey', 'myAccount'], + Authentication: [ + 'loginWithRedirect', + 'handleRedirectCallback', + 'loginWithPopup', + 'logout' + ], + Tokens: [ + 'getAccessTokenSilently', + 'getAccessTokenWithPopup', + 'getIdTokenClaims', + 'revokeRefreshToken', + 'loginWithCustomTokenExchange', + 'customTokenExchange', + 'exchangeToken' + ], + 'Connected Accounts': ['connectAccountWithRedirect'] +}; + +/** Reverse lookup: member name -> category title. */ +const CONTEXT_MEMBER_CATEGORY = new Map( + Object.entries(CONTEXT_MEMBER_CATEGORIES).flatMap(([title, names]) => + names.map(name => [name, title]) + ) +); + +/** Order of the member categories on the `Auth0ContextInterface` page. */ +const MEMBER_CATEGORY_ORDER = [ + 'Auth State', + 'Sub-clients', + 'Authentication', + 'Tokens', + 'User Profile', + 'Connected Accounts', + 'Advanced' +]; + +/** + * Sidebar position for a context member: category order first, then the order + * the names are declared within that category. Uncategorized members + * ("Advanced") sort last, among themselves alphabetically. + */ +const MEMBER_RANK = new Map( + MEMBER_CATEGORY_ORDER.flatMap((title, categoryIndex) => + (CONTEXT_MEMBER_CATEGORIES[title] ?? []).map((name, index) => [ + name, + categoryIndex * 100 + index + ]) + ) +); + +/** @param {string} name */ +function memberRank(name) { + return MEMBER_RANK.get(name) ?? Number.MAX_SAFE_INTEGER; +} + +/** + * Category tags we ignore rather than honour. `ClientConfiguration` ships an + * `@category Main` from `@auth0/auth0-spa-js`, which would otherwise strand it + * in a one-entry "Main" group of its own. + */ +const IGNORED_TAG_CATEGORIES = new Set(['Main']); + +/** + * Stamp an `@category` tag on a reflection, unless the source already declares + * a usable one: a hand-written tag wins, except for the upstream tags above. + * + * @param {import('typedoc').DeclarationReflection} reflection + * @param {string} category + */ +function setCategory(reflection, category) { + const comment = reflection.comment ?? reflection.signatures?.[0]?.comment; + const existing = comment?.getTag('@category'); + + if (existing) { + const text = Comment.combineDisplayParts(existing.content).trim(); + if (!IGNORED_TAG_CATEGORIES.has(text)) return; + comment.removeTags('@category'); + } + + const tag = new CommentTag('@category', [{ kind: 'text', text: category }]); + + if (comment) { + comment.blockTags.push(tag); + } else { + // Undocumented symbol: give it a comment so it can still be grouped. + reflection.comment = new Comment([], [tag]); + } +} + +/** @param {import('typedoc').Application} app */ +function load(app) { + // Priority 1000 so this runs before the built-in CategoryPlugin, which also + // listens on RESOLVE_END and reads (then strips) `@category` tags. + app.converter.on( + Converter.EVENT_RESOLVE_END, + context => { + const { project } = context; + + for (const child of project.children ?? []) { + setCategory(child, categoryFor(child)); + } + + for (const name of ENTRY_INTERFACES) { + const entry = project.getChildByName(name); + for (const member of entry?.children ?? []) { + setCategory( + member, + CONTEXT_MEMBER_CATEGORY.get(member.name) ?? 'Advanced' + ); + } + } + }, + undefined, + 1000 + ); + + app.renderer.defineTheme( + 'auth0', + class extends DefaultTheme { + buildNavigation(project) { + const navigation = super.buildNavigation(project); + addEntryMembers(navigation, project, this.router); + return navigation; + } + } + ); + + // The sidebar is built client-side and every group starts collapsed, so a + // first-time reader lands on a list of category names with nothing in sight. + // Seed the two groups people arrive for as expanded before the nav script + // runs. Reading the key first means a reader who collapses one keeps that + // choice. The key is `data-key` on the accordion, which the nav builder sets + // to the ancestor titles joined by `$`; the lowercase-dashed variant is the + // fallback derivation, seeded too so a change in either direction still works. + const expandKeys = [SETUP, HOOKS].flatMap(title => [ + title, + title.replace(/\s+/g, '-').toLowerCase() + ]); + + app.renderer.hooks.on('body.begin', () => + JSX.createElement( + 'script', + null, + JSX.createElement(JSX.Raw, { + html: `try{${JSON.stringify(expandKeys)}.forEach(function(t){var k='tsd-accordion-'+t;if(localStorage.getItem(k)===null)localStorage.setItem(k,'true')})}catch(e){}` + }) + ) + ); +} + +/** + * Walk the navigation tree and hang each entry interface's members off its node. + * + * @param {any[]} nodes + * @param {import('typedoc').ProjectReflection} project + * @param {import('typedoc').Router} router + */ +function addEntryMembers(nodes, project, router) { + for (const node of nodes) { + if (node.children?.length) { + addEntryMembers(node.children, project, router); + continue; + } + + if (!ENTRY_INTERFACES.includes(node.text)) continue; + + const owner = project.getChildByName(node.text); + if (!owner?.children) continue; + + const members = owner.children.filter( + member => + member.kindOf( + ReflectionKind.Method | + ReflectionKind.Accessor | + ReflectionKind.Property + ) && + !member.flags.isPrivate && + !member.flags.isProtected && + member.name !== 'constructor' + ); + + // 25+ members, so list them in the same task order as the page index: auth + // state and `loginWithRedirect` first, the DPoP escape hatches last. + // Alphabetical would bury the ones most people came for. + members.sort((a, b) => memberRank(a.name) - memberRank(b.name)); + + // Ask the router for the href. TypeDoc 0.28 moved URL assignment out of the + // reflections and behind the Router, so `member.url` and `member.anchor` are + // both undefined here: building the path by hand produced links reading + // `docs/undefined#isLoading`. `getFullUrl` is what TypeDoc's own frontend + // uses for nav entries, and it already includes the anchor. + const links = members + .filter(member => router.hasUrl(member)) + .map(member => ({ + text: member.name, + path: router.getFullUrl(member), + kind: member.kind, + class: member.isDeprecated() ? 'deprecated' : undefined + })); + + if (links.length) { + node.children = links; + } + } +} + +/** + * `categoryOrder` is a single global setting, so it has to cover both the + * top-level export categories and the context interface's member categories. + * The two sets are disjoint, so concatenating them orders each page correctly. + */ +const ALL_CATEGORY_ORDER = [...MEMBER_CATEGORY_ORDER, ...CATEGORY_ORDER]; + +module.exports = { load, CATEGORY_ORDER: ALL_CATEGORY_ORDER }; diff --git a/typedoc.docsv2.js b/typedoc.docsv2.js new file mode 100644 index 00000000..f6f02f93 --- /dev/null +++ b/typedoc.docsv2.js @@ -0,0 +1,41 @@ +// TypeDoc config for the artifact published to the docs-v2 repo. +// +// Reuses every filtering and categorization decision from `typedoc.js` so the +// Mintlify site documents exactly the same surface as the HTML site, but emits +// only the JSON artifact that Mintlify consumes. +// +// `out`, `cleanOutputDir`, `theme` and `customCss` are dropped deliberately. +// TypeDoc's CLI renders HTML whenever `out` is set, even alongside `--json` +// (`if (!json || app.options.isSet("out"))`), so leaving it in would rebuild +// `docs/` as a side effect of building the Mintlify artifact. +const { + out, + cleanOutputDir, + theme, + customCss, + ...shared +} = require('./typedoc.js'); + +module.exports = { + ...shared, + + // Mintlify only reads a symbol's comment summary, so cross-type links have to + // be injected there as markdown. See `scripts/typedoc-plugin-mintlify.js`. + plugin: [...shared.plugin, './scripts/typedoc-plugin-mintlify.js'], + + // Must match `sdk.directory` in the docs-v2 SDK Reference tab and the + // `directory` in the docs-content-pipeline's `auth0-react-generation.js`, + // which builds the curated sidebar from this artifact. The plugin bakes + // absolute hrefs into comment summaries, so a mismatch means every type link + // 404s. + mintlifyDirectory: 'docs/sdk/auth0-react', + + // Staging directory whose layout mirrors docs-v2's `main/`, so publishing is a + // straight copy of two files with no path rewriting. + json: './mintlify/docsv2/sdk-artifacts/auth0-react.json', + + // This file is committed to docs-v2. Minified, a rebuild turns into a + // single-line diff of tens of thousands of changes; pretty-printed, it diffs + // as just the symbols that actually changed. + pretty: true +}; diff --git a/typedoc.js b/typedoc.js index 2800dff2..3e9ca574 100644 --- a/typedoc.js +++ b/typedoc.js @@ -1,15 +1,78 @@ +const { CATEGORY_ORDER } = require('./scripts/typedoc-plugin.js'); + module.exports = { - name: '@auth0/auth0-react', + // Document what the package actually exports. Pointing TypeDoc at `src/` + // instead made it expand every file in the tree, which pulled internal + // modules (the reducer, auth state, utils) into the reference. + entryPoints: ['./src/index.tsx'], + entryPointStrategy: 'resolve', + out: './docs/', - exclude: ['./src/utils.tsx', './src/reducer.tsx'], - excludeExternals: false, + readme: './README.md', + name: 'Auth0 React SDK', + cleanOutputDir: true, + + plugin: ['./scripts/typedoc-plugin.js'], + theme: 'auth0', + + // Keep the reference to the public surface. excludePrivate: true, + excludeProtected: true, + excludeInternal: true, + // Without this, every error class inherits Error's `stack`, `captureStackTrace` + // and `prepareStackTrace` from TypeScript's own lib types, and React's own + // types leak in through the component props. Almost the entire type surface is + // re-exported from `@auth0/auth0-spa-js` and is part of our public API, so + // only the TypeScript lib, React and other third-party packages are external. + excludeExternals: true, + externalPattern: [ + '**/node_modules/typescript/**', + '**/node_modules/@types/**', + '**/node_modules/react/**', + '**/node_modules/react-dom/**' + ], + // Note: no blanket `node_modules` exclusion here. `exclude` is matched against + // a symbol's declaration file, so excluding node_modules would also drop the + // options, errors and sub-clients we deliberately re-export from + // `@auth0/auth0-spa-js`. + exclude: [ + '**/__tests__/**/*', + '**/__mocks__/**/*', + '**/cypress/**/*', + './src/utils.tsx', + './src/reducer.tsx' + ], + + // Group the landing page and sidebar by category rather than by TypeScript + // kind, so readers see "Getting Started" before a wall of interfaces. + categorizeByGroup: false, + categoryOrder: CATEGORY_ORDER, + defaultCategory: 'Other Types', + navigation: { + includeCategories: true, + includeGroups: false + }, + sort: ['kind', 'alphabetical'], + kindSortOrder: [ + 'Function', + 'Class', + 'Interface', + 'TypeAlias', + 'Enum', + 'Variable' + ], + hideGenerator: true, - readme: './README.md', + searchInComments: true, highlightLanguages: ['typescript', 'javascript', 'jsx', 'tsx', 'bash'], + visibilityFilters: { protected: false, inherited: true, - external: true, + external: true }, + + compilerOptions: { + skipLibCheck: true + } }; From 10eb4dafeb3268911e569920d22528b39f28bcd4 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 30 Sep 2026 11:05:21 +0530 Subject: [PATCH 2/4] docs(reference): group API reference into readable sections Shape the generated TypeDoc reference so the landing page and sidebar read as Getting Started / Hooks & HOCs / Context / Errors / Reference instead of one flat alphabetical list of ~100 symbols, and surface the Auth0ContextInterface members directly in the sidebar. - Tag every top-level export and context member with @category where it is declared; re-exports and error classes are placed by rule. - Rewrite the TypeDoc plugin around three categorization rules with a build-failing guardrail: a symbol this SDK declares that no rule places fails the build rather than drifting into a default section. - Discriminate own symbols from dependency re-exports by source path outside node_modules (survives a basePath shift) with a zero-count backstop, so the guardrail cannot be silently defeated. - Order context members by category then declaration line; member source order is left as-is, so the sidebar reflects the existing order. - Ignore the docs:docsv2 artefact directory; align React dev deps to main. --- .gitignore | 3 + package.json | 8 +- scripts/typedoc-plugin.js | 469 +++++++++++++-------------- src/auth-state.tsx | 29 +- src/auth0-context.tsx | 67 +++- src/auth0-provider.tsx | 12 + src/errors.tsx | 2 + src/index.tsx | 11 + src/use-auth0-suspense.tsx | 4 + src/use-auth0.tsx | 29 +- src/use-enterprise-connect.tsx | 4 + src/with-auth0.tsx | 4 + src/with-authentication-required.tsx | 4 + typedoc.js | 7 +- 14 files changed, 387 insertions(+), 266 deletions(-) diff --git a/.gitignore b/.gitignore index 930c9799..f4f9fa2d 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ test-results cypress/screenshots cypress/videos + +# Docs artefact produced by `npm run docs:docsv2` (see typedoc.docsv2.js `json`) +mintlify/docsv2/sdk-artifacts/ diff --git a/package.json b/package.json index e4047413..3f5f4af5 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.3", "@types/jest": "^29.5.14", - "@types/react": "19.2.18", - "@types/react-dom": "19.2.7", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "cypress": "^15.18.0", @@ -76,8 +76,8 @@ "oidc-provider": "^8.8.1", "prettier": "^2.8.1", "pretty-quick": "^3.1.3", - "react": "19.2.8", - "react-dom": "19.2.8", + "react": "19.3.0", + "react-dom": "19.3.0", "rollup": "^3.7.0", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-delete": "^2.0.0", diff --git a/scripts/typedoc-plugin.js b/scripts/typedoc-plugin.js index 10aa794e..959506ff 100644 --- a/scripts/typedoc-plugin.js +++ b/scripts/typedoc-plugin.js @@ -5,13 +5,14 @@ * Two jobs: * * 1. Categorize every top-level export so the landing page and sidebar read as - * "Getting Started / Hooks & HOCs / Context / Errors / ..." instead of one - * flat alphabetical list of ~110 symbols. Categories are derived from the - * export's name and source path, so new exports get sorted automatically - * without anyone having to add an `@category` tag by hand. A tag written in - * the source always wins. + * "Getting Started / Hooks & HOCs / Context / Errors / Reference" instead of + * one flat alphabetical list of ~100 symbols. The symbols this SDK declares + * are tagged where they are declared; everything else is placed by two rules + * keyed on the symbol's name and declaration file. A symbol declared here + * that no rule places fails the build rather than drifting into a default + * section, which is how this SDK's own hooks ended up in "Other Types". * - * 2. Put the context interfaces' members directly in the sidebar, so + * 2. Put the context interface's members directly in the sidebar, so * `getAccessTokenSilently` or `loginWithRedirect` is one click from anywhere * rather than "click the interface, then scan an index, then click again". * The default theme stops the navigation tree at module level, so we extend @@ -23,7 +24,7 @@ const { Converter, DefaultTheme, JSX, - ReflectionKind + ReflectionKind, } = require('typedoc'); /** @@ -32,209 +33,62 @@ const { */ const ENTRY_INTERFACES = ['Auth0ContextInterface']; +/** + * How a re-export is told apart from a symbol this SDK declares: its source + * file resolves inside `node_modules`. Keying on this rather than on a `src/` + * prefix is deliberate. The prefix is relative to TypeDoc's derived basePath, + * so if that anchor ever shifts (a config or layout change), own symbols stop + * matching and get silently reclassified as `Reference`, skipping the validation + * that is the whole point of the guardrail. `node_modules` is in the absolute + * path either way, so the test survives a basePath move. `ownSymbolCount` below + * is the backstop: if the set of own symbols ever empties, the build fails + * rather than passing with everything mislabelled. + */ +const DEPENDENCY_SOURCE_MARKER = 'node_modules'; + +/** Named in diagnostics so the fix is obvious: put an `@category` in `src/`. */ +const OWN_SOURCE_DIR = 'src/'; + const SETUP = 'Getting Started'; const HOOKS = 'Hooks & HOCs'; const CONTEXT = 'Context'; -const CONFIGURATION = 'Configuration'; -const AUTHENTICATION = 'Login & Logout'; -const TOKENS = 'Tokens & Users'; -const MFA = 'Multi-Factor Authentication'; -const PASSKEYS = 'Passkeys'; -const MY_ACCOUNT = 'My Account'; const ERRORS = 'Errors'; -const CACHING = 'Caching'; -const OTHER = 'Other Types'; +const REFERENCE = 'Reference'; + +/** + * Where a symbol lands if it escapes every rule. The validation below makes + * that unreachable, so anything showing up here is a bug in this plugin. + */ +const DEFAULT_CATEGORY = 'Other Types'; /** - * Category order on the landing page and in the sidebar. `*` is where any - * category not listed here lands. + * Section order on the landing page and in the sidebar, following the order the + * reference is read: the provider, then the hooks, then the context, then the + * errors you catch. `Reference` is last because it is reached from a signature, + * never by browsing. `*` is where any category not listed here lands. */ const CATEGORY_ORDER = [ SETUP, HOOKS, CONTEXT, - CONFIGURATION, - AUTHENTICATION, - TOKENS, - MFA, - PASSKEYS, - MY_ACCOUNT, ERRORS, - CACHING, + REFERENCE, '*', - OTHER + DEFAULT_CATEGORY, ]; -/** The provider and the props you hand it: the first thing anyone reads. */ -const SETUP_EXPORTS = new Set([ - 'Auth0Provider', - 'Auth0ProviderOptions', - 'Auth0ProviderWithConfigOptions', - 'Auth0ProviderWithClientOptions', - 'AppState' -]); - -/** The consumption surface: hooks and higher-order components. */ -const HOOKS_EXPORTS = new Set([ - 'useAuth0', - 'useAuth0Suspense', - 'withAuth0', - 'WithAuth0Props', - 'withAuthenticationRequired', - 'WithAuthenticationRequiredOptions' -]); - -/** - * The context object and the shapes it carries. `initialContext` is exported but - * carries `@ignore`, so it never reaches the reference. - */ -const CONTEXT_EXPORTS = new Set([ - 'Auth0Context', - 'Auth0ContextInterface', - 'Auth0SuspenseContextInterface' -]); - -/** Exports that belong in "Configuration" regardless of kind. */ -const CONFIGURATION_EXPORTS = new Set([ - 'AuthorizationParams', - 'ClientConfiguration', - 'CacheLocation', - 'RefreshTokenMode', - 'ResponseType', - 'InteractiveErrorHandler' -]); - -/** Options and results for the login, logout and connect-account flows. */ -const AUTHENTICATION_EXPORTS = new Set([ - 'RedirectLoginOptions', - 'RedirectLoginResult', - 'PopupLoginOptions', - 'PopupConfigOptions', - 'LogoutOptions', - 'LogoutUrlOptions', - 'RedirectConnectAccountOptions', - 'ConnectAccountRedirectResult', - 'ConnectedAccount' -]); - -/** Everything about acquiring tokens and reading the resulting identity. */ -const TOKEN_EXPORTS = new Set([ - 'GetTokenSilentlyOptions', - 'GetTokenWithPopupOptions', - 'TokenEndpointResponse', - 'RevokeRefreshTokenOptions', - 'CustomTokenExchangeOptions', - 'User', - 'IdToken', - 'ActClaim', - 'FetcherConfig' -]); - -/** Cache implementations and the interface they satisfy. */ -const CACHING_EXPORTS = new Set([ - 'ICache', - 'InMemoryCache', - 'LocalStorageCache', - 'Cacheable' -]); - -/** Names in "My Account" that carry no `MyAccount` prefix to match on. */ -const MY_ACCOUNT_EXPORTS = new Set([ - 'AuthenticationMethod', - 'AuthenticationMethodType', - 'Factor', - 'UpdateAuthenticationMethodRequest', - 'EnrollmentChallengeOptions', - 'EnrollmentChallengeResponse', - 'EnrollmentVerifyOptions' -]); - /** - * Decide which category a top-level export belongs to. Driven by name and - * source path so that new exports land somewhere sensible on their own. - * - * @param {import('typedoc').DeclarationReflection} reflection - * @returns {string} + * The only categories a top-level export may be tagged with. Anything else is a + * typo, which would otherwise render as a plausible-looking one-entry section. */ -function categoryFor(reflection) { - const { name } = reflection; - - if (SETUP_EXPORTS.has(name)) return SETUP; - if (HOOKS_EXPORTS.has(name)) return HOOKS; - if (CONTEXT_EXPORTS.has(name)) return CONTEXT; - - // Errors first: an error's home is the Errors section even when a feature - // prefix below would otherwise claim it (MfaVerifyError, PasskeyError, ...). - if (/Error$/.test(name) || name === 'MfaRequirements') return ERRORS; - - if (CONFIGURATION_EXPORTS.has(name)) return CONFIGURATION; - if (AUTHENTICATION_EXPORTS.has(name)) return AUTHENTICATION; - if (TOKEN_EXPORTS.has(name)) return TOKENS; - if (CACHING_EXPORTS.has(name)) return CACHING; - if (MY_ACCOUNT_EXPORTS.has(name)) return MY_ACCOUNT; - - // Nearly everything else is re-exported from `@auth0/auth0-spa-js`, so there - // is no path under `src/` to match on: the name is all we have. - if (name.startsWith('MyAccount')) return MY_ACCOUNT; - if (name.startsWith('Passkey')) return PASSKEYS; - if (name.startsWith('Mfa') || name.startsWith('Enroll')) return MFA; - if ( - name === 'Authenticator' || - name === 'ChallengeAuthenticatorParams' || - name === 'ChallengeResponse' || - name === 'VerifyParams' - ) { - return MFA; - } - - // Source paths are relative to TypeDoc's computed base path, which shifts - // depending on which files end up in the program, so match on the directory - // segment rather than a prefix. - const fileName = reflection.sources?.[0]?.fileName ?? ''; - const inDir = dir => fileName.includes(`${dir}/`); - - if (inDir('mfa')) return MFA; - if (inDir('passkey')) return PASSKEYS; - if (inDir('myaccount')) return MY_ACCOUNT; - if (inDir('cache')) return CACHING; - - return OTHER; -} +const TOP_LEVEL_CATEGORIES = [SETUP, HOOKS, CONTEXT, ERRORS, REFERENCE]; /** - * Categories for `Auth0ContextInterface`'s own members, so its page groups 25+ - * entries by task instead of listing them all under one "Properties" heading. - * Anything not listed here falls into "Advanced". + * Order of the member categories on the `Auth0ContextInterface` page, and the + * only categories a member may be tagged with. Kept separate from the top-level + * set so that tagging a member with a section name, or the reverse, is caught + * rather than silently filed in the wrong place. */ -const CONTEXT_MEMBER_CATEGORIES = { - 'Auth State': ['isLoading', 'isAuthenticated', 'user', 'error'], - 'Sub-clients': ['mfa', 'passkey', 'myAccount'], - Authentication: [ - 'loginWithRedirect', - 'handleRedirectCallback', - 'loginWithPopup', - 'logout' - ], - Tokens: [ - 'getAccessTokenSilently', - 'getAccessTokenWithPopup', - 'getIdTokenClaims', - 'revokeRefreshToken', - 'loginWithCustomTokenExchange', - 'customTokenExchange', - 'exchangeToken' - ], - 'Connected Accounts': ['connectAccountWithRedirect'] -}; - -/** Reverse lookup: member name -> category title. */ -const CONTEXT_MEMBER_CATEGORY = new Map( - Object.entries(CONTEXT_MEMBER_CATEGORIES).flatMap(([title, names]) => - names.map(name => [name, title]) - ) -); - -/** Order of the member categories on the `Auth0ContextInterface` page. */ const MEMBER_CATEGORY_ORDER = [ 'Auth State', 'Sub-clients', @@ -242,55 +96,63 @@ const MEMBER_CATEGORY_ORDER = [ 'Tokens', 'User Profile', 'Connected Accounts', - 'Advanced' + 'Advanced', ]; /** - * Sidebar position for a context member: category order first, then the order - * the names are declared within that category. Uncategorized members - * ("Advanced") sort last, among themselves alphabetically. + * Context member name -> sidebar position, filled while the `@category` tags + * still exist. The renderer needs this ordering after TypeDoc's own category + * plugin has read and stripped the tags, so it cannot recompute it there. + */ +const memberSidebarOrder = new Map(); + +/** + * @param {import('typedoc').DeclarationReflection} reflection + * @returns {string} */ -const MEMBER_RANK = new Map( - MEMBER_CATEGORY_ORDER.flatMap((title, categoryIndex) => - (CONTEXT_MEMBER_CATEGORIES[title] ?? []).map((name, index) => [ - name, - categoryIndex * 100 + index - ]) - ) -); - -/** @param {string} name */ -function memberRank(name) { - return MEMBER_RANK.get(name) ?? Number.MAX_SAFE_INTEGER; +function sourceFile(reflection) { + return reflection.sources?.[0]?.fileName ?? ''; +} + +/** @param {import('typedoc').DeclarationReflection} reflection */ +function declaredHere(reflection) { + return !sourceFile(reflection).includes(DEPENDENCY_SOURCE_MARKER); } /** - * Category tags we ignore rather than honour. `ClientConfiguration` ships an - * `@category Main` from `@auth0/auth0-spa-js`, which would otherwise strand it - * in a one-entry "Main" group of its own. + * The comment carrying the tags. For an arrow-function export the doc block + * attaches to the signature rather than the declaration. + * + * @param {import('typedoc').DeclarationReflection} reflection */ -const IGNORED_TAG_CATEGORIES = new Set(['Main']); +function commentOf(reflection) { + return reflection.comment ?? reflection.signatures?.[0]?.comment; +} /** - * Stamp an `@category` tag on a reflection, unless the source already declares - * a usable one: a hand-written tag wins, except for the upstream tags above. + * The category written in the source, if any. + * + * @param {import('typedoc').DeclarationReflection} reflection + * @returns {string | undefined} + */ +function writtenCategory(reflection) { + const tag = commentOf(reflection)?.getTag('@category'); + return tag ? Comment.combineDisplayParts(tag.content).trim() : undefined; +} + +/** + * Stamp an `@category` tag on a reflection, replacing whatever is already + * there. * * @param {import('typedoc').DeclarationReflection} reflection * @param {string} category */ function setCategory(reflection, category) { - const comment = reflection.comment ?? reflection.signatures?.[0]?.comment; - const existing = comment?.getTag('@category'); - - if (existing) { - const text = Comment.combineDisplayParts(existing.content).trim(); - if (!IGNORED_TAG_CATEGORIES.has(text)) return; - comment.removeTags('@category'); - } - + const comment = commentOf(reflection); const tag = new CommentTag('@category', [{ kind: 'text', text: category }]); if (comment) { + comment.removeTags('@category'); comment.blockTags.push(tag); } else { // Undocumented symbol: give it a comment so it can still be grouped. @@ -298,28 +160,148 @@ function setCategory(reflection, category) { } } +/** + * Decide which category a symbol belongs to. Three rules, first match wins: + * + * 1. A tag written on a declaration in `src/`, which is kept and validated. + * Tags on a declaration outside `src/` are not rule 1: they came from a + * dependency, and rule 3 discards them. + * 2. A name ending in `Error`, which is an error class no matter who declared + * it. This precedes rule 3 because every error class is also a re-export, and + * it tests the name rather than trusting upstream's tags because upstream + * tags only one of its several error modules. + * 3. Declared outside `src/`, which makes it a supporting type reached from a + * signature, so it goes to `Reference`. This *overwrites* any category the + * symbol arrived with: TypeDoc reads `@category` out of a dependency's type + * declarations, so upstream categories turn up here whether or not we want + * them, and leaving them would strand symbols in unlisted sections. + * + * Nothing left over is legitimate: a symbol this SDK declares and no rule places + * is the drift this plugin exists to catch, so it becomes a build error. + * + * @param {import('typedoc').DeclarationReflection} reflection + * @param {string[]} allowed Categories this reflection may be tagged with. + * @param {string[]} problems Collects anything that should fail the build. + * @returns {string} the category the symbol ended up in + */ +function categorize(reflection, allowed, problems) { + const here = declaredHere(reflection); + const written = here ? writtenCategory(reflection) : undefined; + + if (written) { + if (!allowed.includes(written)) { + problems.push( + `${reflection.name} (${sourceFile(reflection)}) is tagged ` + + `"@category ${written}", which is not a category this SDK defines ` + + `here. Expected one of: ${allowed.join(', ')}.` + ); + } + return written; + } + + if (/Error$/.test(reflection.name)) { + setCategory(reflection, ERRORS); + return ERRORS; + } + + if (!here) { + setCategory(reflection, REFERENCE); + return REFERENCE; + } + + problems.push( + `${reflection.name} (${sourceFile(reflection)}) has no @category tag. ` + + `Every symbol declared in ${OWN_SOURCE_DIR} needs one: ` + + `${allowed.join(', ')}.` + ); + return DEFAULT_CATEGORY; +} + +/** @param {import('typedoc').DeclarationReflection} reflection */ +function memberCategoryIndex(reflection) { + const written = writtenCategory(reflection) ?? ''; + const index = MEMBER_CATEGORY_ORDER.indexOf(written); + return index === -1 ? MEMBER_CATEGORY_ORDER.length : index; +} + +/** + * Sidebar order for the context members: by category, then by the order they + * are declared within that category. Alphabetical would bury the ones most + * people came for under the DPoP escape hatches. + * + * Declaration order means the line the member is written on, so moving a member + * within the interface moves it in the sidebar. That holds only while every + * member of a category is declared in one file, which is true today: the auth + * state is all of `auth-state.tsx` and the rest is all of `auth0-context.tsx`. + * + * @param {import('typedoc').DeclarationReflection} a + * @param {import('typedoc').DeclarationReflection} b + */ +function compareMembers(a, b) { + const byCategory = memberCategoryIndex(a) - memberCategoryIndex(b); + if (byCategory !== 0) return byCategory; + return (a.sources?.[0]?.line ?? 0) - (b.sources?.[0]?.line ?? 0); +} + /** @param {import('typedoc').Application} app */ function load(app) { // Priority 1000 so this runs before the built-in CategoryPlugin, which also // listens on RESOLVE_END and reads (then strips) `@category` tags. app.converter.on( Converter.EVENT_RESOLVE_END, - context => { + (context) => { const { project } = context; + /** @type {string[]} */ + const problems = []; + let referenceCount = 0; + let reExportCount = 0; + let ownSymbolCount = 0; for (const child of project.children ?? []) { - setCategory(child, categoryFor(child)); + if (declaredHere(child)) ownSymbolCount++; + if (categorize(child, TOP_LEVEL_CATEGORIES, problems) === REFERENCE) { + referenceCount++; + if (!declaredHere(child)) reExportCount++; + } + } + + // Backstop for the `declaredHere` test: this SDK always declares its own + // top-level exports, so a count of zero means the discriminator stopped + // recognising them and every symbol slipped into `Reference` unvalidated. + // Fail loudly rather than ship a reference with no sections. + if ((project.children?.length ?? 0) > 0 && ownSymbolCount === 0) { + problems.push( + 'No top-level export was recognised as declared in this SDK. The ' + + 'test for own symbols (source path outside ' + + `"${DEPENDENCY_SOURCE_MARKER}") is matching nothing, so category ` + + 'validation never ran. This is a plugin bug, not a docs error.' + ); } for (const name of ENTRY_INTERFACES) { const entry = project.getChildByName(name); - for (const member of entry?.children ?? []) { - setCategory( - member, - CONTEXT_MEMBER_CATEGORY.get(member.name) ?? 'Advanced' - ); + const members = [...(entry?.children ?? [])]; + + for (const member of members) { + categorize(member, MEMBER_CATEGORY_ORDER, problems); } + + members.sort(compareMembers); + members.forEach((member, index) => + memberSidebarOrder.set(member.name, index) + ); } + + for (const problem of problems) { + app.logger.error(problem); + } + + // One summary line, not one per symbol: a jump in this count means a + // dependency added exports, which is the only thing worth noticing here. + app.logger.info( + `${REFERENCE} holds ${referenceCount} symbols, ` + + `${reExportCount} of them re-exported from dependencies.` + ); }, undefined, 1000 @@ -343,9 +325,9 @@ function load(app) { // choice. The key is `data-key` on the accordion, which the nav builder sets // to the ancestor titles joined by `$`; the lowercase-dashed variant is the // fallback derivation, seeded too so a change in either direction still works. - const expandKeys = [SETUP, HOOKS].flatMap(title => [ + const expandKeys = [SETUP, HOOKS].flatMap((title) => [ title, - title.replace(/\s+/g, '-').toLowerCase() + title.replace(/\s+/g, '-').toLowerCase(), ]); app.renderer.hooks.on('body.begin', () => @@ -353,7 +335,9 @@ function load(app) { 'script', null, JSX.createElement(JSX.Raw, { - html: `try{${JSON.stringify(expandKeys)}.forEach(function(t){var k='tsd-accordion-'+t;if(localStorage.getItem(k)===null)localStorage.setItem(k,'true')})}catch(e){}` + html: `try{${JSON.stringify( + expandKeys + )}.forEach(function(t){var k='tsd-accordion-'+t;if(localStorage.getItem(k)===null)localStorage.setItem(k,'true')})}catch(e){}`, }) ) ); @@ -379,7 +363,7 @@ function addEntryMembers(nodes, project, router) { if (!owner?.children) continue; const members = owner.children.filter( - member => + (member) => member.kindOf( ReflectionKind.Method | ReflectionKind.Accessor | @@ -390,10 +374,11 @@ function addEntryMembers(nodes, project, router) { member.name !== 'constructor' ); - // 25+ members, so list them in the same task order as the page index: auth - // state and `loginWithRedirect` first, the DPoP escape hatches last. - // Alphabetical would bury the ones most people came for. - members.sort((a, b) => memberRank(a.name) - memberRank(b.name)); + members.sort( + (a, b) => + (memberSidebarOrder.get(a.name) ?? Number.MAX_SAFE_INTEGER) - + (memberSidebarOrder.get(b.name) ?? Number.MAX_SAFE_INTEGER) + ); // Ask the router for the href. TypeDoc 0.28 moved URL assignment out of the // reflections and behind the Router, so `member.url` and `member.anchor` are @@ -401,12 +386,12 @@ function addEntryMembers(nodes, project, router) { // `docs/undefined#isLoading`. `getFullUrl` is what TypeDoc's own frontend // uses for nav entries, and it already includes the anchor. const links = members - .filter(member => router.hasUrl(member)) - .map(member => ({ + .filter((member) => router.hasUrl(member)) + .map((member) => ({ text: member.name, path: router.getFullUrl(member), kind: member.kind, - class: member.isDeprecated() ? 'deprecated' : undefined + class: member.isDeprecated() ? 'deprecated' : undefined, })); if (links.length) { @@ -422,4 +407,8 @@ function addEntryMembers(nodes, project, router) { */ const ALL_CATEGORY_ORDER = [...MEMBER_CATEGORY_ORDER, ...CATEGORY_ORDER]; -module.exports = { load, CATEGORY_ORDER: ALL_CATEGORY_ORDER }; +module.exports = { + load, + CATEGORY_ORDER: ALL_CATEGORY_ORDER, + DEFAULT_CATEGORY, +}; diff --git a/src/auth-state.tsx b/src/auth-state.tsx index 70610f23..63f43040 100644 --- a/src/auth-state.tsx +++ b/src/auth-state.tsx @@ -4,10 +4,35 @@ import { User } from '@auth0/auth0-spa-js'; * The auth state which, when combined with the auth methods, make up the return object of the `useAuth0` hook. */ export interface AuthState { - error: Error | undefined; - isAuthenticated: boolean; + /** + * `true` while the SDK is initialising or a redirect callback is being + * handled. Render a loading state rather than trusting `isAuthenticated` + * until this is `false`. + * + * @category Auth State + */ isLoading: boolean; + + /** + * `true` once there is a valid session for the user. + * + * @category Auth State + */ + isAuthenticated: boolean; + + /** + * The authenticated user's profile, or `undefined` when there is no session. + * + * @category Auth State + */ user: TUser | undefined; + + /** + * The error from the last failed authentication attempt, if any. + * + * @category Auth State + */ + error: Error | undefined; } /** diff --git a/src/auth0-context.tsx b/src/auth0-context.tsx index fa040e4d..415179a6 100644 --- a/src/auth0-context.tsx +++ b/src/auth0-context.tsx @@ -22,14 +22,33 @@ import { createContext } from 'react'; import { AuthState, initialAuthState } from './auth-state'; import { AppState } from './auth0-provider'; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type +/* eslint-disable @typescript-eslint/no-empty-object-type -- both narrow an + upstream type by dropping the SDK-managed `onRedirect`, so they add no members + of their own. A disable block rather than per-line comments: an interleaved + line comment between the TSDoc and the declaration hides the `@category` tag + from TypeDoc. */ + +/** + * Options for {@link Auth0ContextInterface.logout}. + * + * @category Reference + */ export interface LogoutOptions extends Omit {} -// eslint-disable-next-line @typescript-eslint/no-empty-object-type + +/** + * Options for {@link Auth0ContextInterface.loginWithRedirect}. + * + * @category Reference + */ export interface RedirectLoginOptions extends Omit, 'onRedirect'> {} +/* eslint-enable @typescript-eslint/no-empty-object-type */ + /** * Contains the authenticated state and authentication methods provided by the `useAuth0` hook. + * + * @category Context */ export interface Auth0ContextInterface extends AuthState { @@ -58,6 +77,8 @@ export interface Auth0ContextInterface * * Note that in all cases, falling back to an iframe requires access to * the `auth0` cookie. + * + * @category Tokens */ getAccessTokenSilently: Auth0Client['getTokenSilently']; @@ -72,6 +93,8 @@ export interface Auth0ContextInterface * provided as arguments. Random and secure `state` and `nonce` * parameters will be auto-generated. If the response is successful, * results will be valid according to their expiration times. + * + * @category Tokens */ getAccessTokenWithPopup: ( options?: GetTokenWithPopupOptions, @@ -84,6 +107,8 @@ export interface Auth0ContextInterface * ``` * * Returns all claims from the id_token if available. + * + * @category User Profile */ getIdTokenClaims: () => Promise; @@ -134,6 +159,8 @@ export interface Auth0ContextInterface * console.error('Token exchange failed:', error); * } * ``` + * + * @category Tokens */ loginWithCustomTokenExchange: ( options: CustomTokenExchangeOptions @@ -161,6 +188,8 @@ export interface Auth0ContextInterface * * @param options - The options required to perform the token exchange. * @returns A promise that resolves to the token endpoint response. + * + * @category Tokens */ customTokenExchange: ( options: CustomTokenExchangeOptions @@ -194,6 +223,8 @@ export interface Auth0ContextInterface * * @param options - The options required to perform the token exchange * @returns A promise that resolves to the token endpoint response containing Auth0 tokens + * + * @category Tokens */ exchangeToken: ( options: CustomTokenExchangeOptions @@ -207,6 +238,8 @@ export interface Auth0ContextInterface * Performs a redirect to `/authorize` using the parameters * provided as arguments. Random and secure `state` and `nonce` * parameters will be auto-generated. + * + * @category Authentication */ loginWithRedirect: ( options?: RedirectLoginOptions @@ -225,6 +258,8 @@ export interface Auth0ContextInterface * IMPORTANT: This method has to be called from an event handler * that was started by the user like a button click, for example, * otherwise the popup will be blocked in most browsers. + * + * @category Authentication */ loginWithPopup: ( options?: PopupLoginOptions, @@ -248,6 +283,8 @@ export interface Auth0ContextInterface * * If connecting the account is successful `onRedirectCallback` will be called * with the details of the connected account. + * + * @category Connected Accounts */ connectAccountWithRedirect: ( options: RedirectConnectAccountOptions @@ -262,6 +299,8 @@ export interface Auth0ContextInterface * the parameters provided as arguments, to clear the Auth0 session. * If the `logoutParams.federated` option is specified, it also clears the Identity Provider session. * [Read more about how Logout works at Auth0](https://auth0.com/docs/logout). + * + * @category Authentication */ logout: (options?: LogoutOptions) => Promise; @@ -291,6 +330,8 @@ export interface Auth0ContextInterface * * @param options - Optional parameters to identify which refresh token to revoke. * Defaults to the audience configured in `authorizationParams`. + * + * @category Tokens */ revokeRefreshToken: (options?: RevokeRefreshTokenOptions) => Promise; @@ -301,6 +342,8 @@ export interface Auth0ContextInterface * will be valid according to their expiration times. * * @param url The URL to that should be used to retrieve the `state` and `code` values. Defaults to `window.location.href` if not given. + * + * @category Authentication */ handleRedirectCallback: (url?: string) => Promise; @@ -316,6 +359,8 @@ export interface Auth0ContextInterface * @param id The identifier of a nonce: if absent, it will get the nonce * used for requests to Auth0. Otherwise, it will be used to * select a specific non-Auth0 nonce. + * + * @category Advanced */ getDpopNonce: Auth0Client['getDpopNonce']; @@ -328,6 +373,8 @@ export interface Auth0ContextInterface * @param id The identifier of a nonce: if absent, it will set the nonce * used for requests to Auth0. Otherwise, it will be used to * select a specific non-Auth0 nonce. + * + * @category Advanced */ setDpopNonce: Auth0Client['setDpopNonce']; @@ -336,6 +383,8 @@ export interface Auth0ContextInterface * key used to cryptographically bind access tokens with DPoP. * * It requires enabling the {@link Auth0ClientOptions.useDpop} option. + * + * @category Advanced */ generateDpopProof: Auth0Client['generateDpopProof']; @@ -346,6 +395,8 @@ export interface Auth0ContextInterface * headers or managing DPoP nonces and retries automatically. * * Check the `EXAMPLES.md` file for a deeper look into this method. + * + * @category Advanced */ createFetcher: Auth0Client['createFetcher']; @@ -357,6 +408,8 @@ export interface Auth0ContextInterface * * Returns a readonly copy of the initialization configuration * containing the domain and clientId. + * + * @category Advanced */ getConfiguration: Auth0Client['getConfiguration']; @@ -411,6 +464,8 @@ export interface Auth0ContextInterface * } * } * ``` + * + * @category Sub-clients */ mfa: MfaApiClient; @@ -427,6 +482,8 @@ export interface Auth0ContextInterface * * Both methods exchange the WebAuthn credential for Auth0 tokens and update * `isAuthenticated` / `user` in the same way as `loginWithPopup`. + * + * @category Sub-clients */ passkey: PasskeyApiClient; @@ -463,6 +520,8 @@ export interface Auth0ContextInterface * // Remove an authentication method * await myAccount.deleteAuthenticationMethod('method-id'); * ``` + * + * @category Sub-clients */ myAccount: MyAccountApiClient; @@ -478,6 +537,8 @@ export interface Auth0ContextInterface * - `getTokenSilently(options?)` - Get or renew an anonymous access token * - `logout()` - End the anonymous session and clear stored tokens * - `getClaims()` - Always returns `null` in EA + * + * @category Sub-clients */ anonymous: AnonymousSessionApiClient; @@ -555,6 +616,8 @@ export const initialContext = { /** * The Auth0 Context + * + * @category Context */ const Auth0Context = createContext(initialContext); diff --git a/src/auth0-provider.tsx b/src/auth0-provider.tsx index bd9331e6..1fe693d4 100644 --- a/src/auth0-provider.tsx +++ b/src/auth0-provider.tsx @@ -41,12 +41,16 @@ import { initialAuthState, type AuthState } from './auth-state'; /** * The account that has been connected during the connect flow. + * + * @category Reference */ export type ConnectedAccount = Omit; /** * The state of the application before the user was redirected to the login page * and any account that the user may have connected to. + * + * @category Getting Started */ export type AppState = { returnTo?: string; @@ -103,12 +107,16 @@ type Auth0ProviderBaseOptions = { /** * Options for `Auth0Provider` when configuring Auth0 via `domain` and `clientId`. * Use this type when building wrapper components around `Auth0Provider`. + * + * @category Getting Started */ export type Auth0ProviderWithConfigOptions = Auth0ProviderBaseOptions & Auth0ClientOptions & { client?: never }; /** * Options for `Auth0Provider` when supplying a pre-configured `Auth0Client` instance. + * + * @category Getting Started */ export type Auth0ProviderWithClientOptions = Auth0ProviderBaseOptions & { client: Auth0Client }; @@ -118,6 +126,8 @@ export type Auth0ProviderWithClientOptions = * * Either provide `domain` and `clientId` (`Auth0ProviderWithConfigOptions`) * or a pre-configured `client` instance (`Auth0ProviderWithClientOptions`). + * + * @category Getting Started */ export type Auth0ProviderOptions = | Auth0ProviderWithConfigOptions @@ -199,6 +209,8 @@ const createInitDeferred = (): InitDeferred => { * `'use client'` directive), so in a Next.js App Router app you can import and render * `Auth0Provider` directly from a Server Component such as `app/layout.tsx`. * Components that call hooks like `useAuth0` must still be Client Components. + * + * @category Getting Started */ const Auth0Provider = (opts: Auth0ProviderOptions) => { const { diff --git a/src/errors.tsx b/src/errors.tsx index 5f242b25..60a4953f 100644 --- a/src/errors.tsx +++ b/src/errors.tsx @@ -3,6 +3,8 @@ * be the error code. And possibly an `error_description` property * * See: https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6 + * + * @category Errors */ export class OAuthError extends Error { constructor(public error: string, public error_description?: string) { diff --git a/src/index.tsx b/src/index.tsx index e8066357..eb3530eb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,14 @@ +/** + * @module + * + * @categoryDescription Reference + * Supporting option, claim and parameter types that come from the underlying + * `@auth0/auth0-spa-js` SDK. You arrive at one of these from the hook or context + * member that uses it, rather than by browsing this section. For deeper + * documentation on each, see the + * [@auth0/auth0-spa-js reference](https://auth0.github.io/auth0-spa-js/). + */ + export { default as Auth0Provider, Auth0ProviderOptions, diff --git a/src/use-auth0-suspense.tsx b/src/use-auth0-suspense.tsx index b86c3418..48ac9c7a 100644 --- a/src/use-auth0-suspense.tsx +++ b/src/use-auth0-suspense.tsx @@ -11,6 +11,8 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; * The value returned by `useAuth0Suspense`: the full `useAuth0` interface minus * `isLoading` and the internal `_initPromise`. `error` is * retained for post-init failures such as `loginWithPopup`. + * + * @category Context */ export type Auth0SuspenseContextInterface = Omit< Auth0ContextInterface, @@ -39,6 +41,8 @@ export type Auth0SuspenseContextInterface = Omit< * renders if that check succeeded, or throws again if it did not. * * TUser is an optional type param to provide a type to the `user` field. + * + * @category Hooks & HOCs */ const useAuth0Suspense = ( context = Auth0Context diff --git a/src/use-auth0.tsx b/src/use-auth0.tsx index 372e3e44..9eee1fe4 100644 --- a/src/use-auth0.tsx +++ b/src/use-auth0.tsx @@ -4,27 +4,24 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; /** * ```js - * const { - * // Auth state: - * error, - * isAuthenticated, - * isLoading, - * user, - * // Auth methods: - * getAccessTokenSilently, - * getAccessTokenWithPopup, - * getIdTokenClaims, - * loginWithCustomTokenExchange, - * exchangeToken, // deprecated - use loginWithCustomTokenExchange - * loginWithRedirect, - * loginWithPopup, - * logout, - * } = useAuth0(); + * const { isLoading, isAuthenticated, user, loginWithRedirect, logout } = + * useAuth0(); * ``` * * Use the `useAuth0` hook in your components to access the auth state and methods. * + * It returns an {@link Auth0ContextInterface}, which carries the auth state + * (`isLoading`, `isAuthenticated`, `user`, `error`), the login, logout and token + * methods, and the four sub-clients: + * {@link Auth0ContextInterface.mfa | mfa}, + * {@link Auth0ContextInterface.passkey | passkey}, + * {@link Auth0ContextInterface.myAccount | myAccount} and + * {@link Auth0ContextInterface.anonymous | anonymous}. See that interface for + * the full set of members. + * * TUser is an optional type param to provide a type to the `user` field. + * + * @category Hooks & HOCs */ const useAuth0 = ( context = Auth0Context diff --git a/src/use-enterprise-connect.tsx b/src/use-enterprise-connect.tsx index 4b0e2496..7cd2e332 100644 --- a/src/use-enterprise-connect.tsx +++ b/src/use-enterprise-connect.tsx @@ -10,6 +10,8 @@ import Auth0Context, { /** * The shape returned by the `useEnterpriseConnect` hook. + * + * @category Hooks & HOCs */ export interface UseEnterpriseConnect { /** @@ -42,6 +44,8 @@ export interface UseEnterpriseConnect { * the Auth0 domain from the `Auth0Provider` configuration, so callers pass * only the email domain. `loginWithSSO` is sugar over `loginWithRedirect` * that sets `login_hint` to the provided email. + * + * @category Hooks & HOCs */ const useEnterpriseConnect = ( context = Auth0Context diff --git a/src/with-auth0.tsx b/src/with-auth0.tsx index 6232e6f2..47a57266 100644 --- a/src/with-auth0.tsx +++ b/src/with-auth0.tsx @@ -3,6 +3,8 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; /** * Components wrapped in `withAuth0` will have an additional `auth0` prop + * + * @category Hooks & HOCs */ export interface WithAuth0Props { auth0: Auth0ContextInterface; @@ -25,6 +27,8 @@ export interface WithAuth0Props { * * Providing a context as the second argument allows you to configure the Auth0Provider the Auth0Context * should come from f you have multiple within your application. + * + * @category Hooks & HOCs */ const withAuth0 =

( Component: ComponentType

, diff --git a/src/with-authentication-required.tsx b/src/with-authentication-required.tsx index 41bc8bed..1df61ebe 100644 --- a/src/with-authentication-required.tsx +++ b/src/with-authentication-required.tsx @@ -29,6 +29,8 @@ const defaultReturnTo = (): string => { /** * Options for the withAuthenticationRequired Higher Order Component + * + * @category Hooks & HOCs */ export interface WithAuthenticationRequiredOptions { /** @@ -99,6 +101,8 @@ export interface WithAuthenticationRequiredOptions { * * When you wrap your components in this Higher Order Component and an anonymous user visits your component * they will be redirected to the login page; after login they will be returned to the page they were redirected from. + * + * @category Hooks & HOCs */ const withAuthenticationRequired =

( Component: ComponentType

, diff --git a/typedoc.js b/typedoc.js index 3e9ca574..674fbecb 100644 --- a/typedoc.js +++ b/typedoc.js @@ -1,4 +1,7 @@ -const { CATEGORY_ORDER } = require('./scripts/typedoc-plugin.js'); +const { + CATEGORY_ORDER, + DEFAULT_CATEGORY +} = require('./scripts/typedoc-plugin.js'); module.exports = { // Document what the package actually exports. Pointing TypeDoc at `src/` @@ -47,7 +50,7 @@ module.exports = { // kind, so readers see "Getting Started" before a wall of interfaces. categorizeByGroup: false, categoryOrder: CATEGORY_ORDER, - defaultCategory: 'Other Types', + defaultCategory: DEFAULT_CATEGORY, navigation: { includeCategories: true, includeGroups: false From b76a496299d3fe4b954e06720873e6967b8d60d5 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 30 Sep 2026 12:39:01 +0530 Subject: [PATCH 3/4] docs(typedoc): remove Mintlify plugin configuration from TypeDoc settings --- scripts/typedoc-plugin-mintlify.js | 141 ----------------------------- typedoc.docsv2.js | 11 --- 2 files changed, 152 deletions(-) delete mode 100644 scripts/typedoc-plugin-mintlify.js diff --git a/scripts/typedoc-plugin-mintlify.js b/scripts/typedoc-plugin-mintlify.js deleted file mode 100644 index 951b0ca4..00000000 --- a/scripts/typedoc-plugin-mintlify.js +++ /dev/null @@ -1,141 +0,0 @@ -// @ts-check -/** - * TypeDoc plugin for the Mintlify SDK reference pipeline. - * - * Mintlify renders a type reference as a plain text pill, so `mfa: MfaApiClient` - * on the `Auth0Client` page is a dead end even though `MfaApiClient` has its own - * generated page. Mintlify's renderer reads only a symbol's comment summary: - * `@see` block tags are dropped and `{@link}` inline tags are resolved by - * TypeDoc before serialization, so the one thing that survives into the page is - * markdown in the summary itself. - * - * This plugin therefore appends a "Type:" line of markdown links to every - * symbol whose type mentions another documented export. It runs only for the - * JSON artifact, never for the HTML site, where TypeDoc already links types. - */ -const { Converter, ParameterType, ReflectionKind, Type } = require('typedoc'); - -/** - * URL segment Mintlify assigns to each page-level `ReflectionKind`. These are - * not documented anywhere; they were read off a generated build. Note they - * differ from TypeDoc's own HTML theme, which uses `enumerations/` and - * `type-aliases/`. - */ -const KIND_SEGMENT = { - [ReflectionKind.Enum]: 'enums', - [ReflectionKind.Variable]: 'variables', - [ReflectionKind.Function]: 'functions', - [ReflectionKind.Class]: 'classes', - [ReflectionKind.Interface]: 'interfaces', - [ReflectionKind.TypeAlias]: 'types' -}; - -/** Kinds whose pages carry the type pills worth linking. */ -const LINKABLE_KINDS = - ReflectionKind.Property | - ReflectionKind.Accessor | - ReflectionKind.Parameter | - ReflectionKind.TypeAlias | - ReflectionKind.Variable; - -/** - * Collect the names of every reference type mentioned anywhere in a type, - * including inside type arguments, unions, intersections and arrays. A - * `Promise` return type should link the - * response, not be skipped for being wrapped. - * - * Recursion is restricted to `Type` instances on purpose. Some types hold a - * `declaration` pointing back at a reflection, and following that walks into - * the whole project graph and overflows the stack. - * - * @param {import('typedoc').SomeType | undefined} type - * @param {Set} out - */ -function collectReferenceNames(type, out) { - if (!(type instanceof Type)) return; - - if (type.type === 'reference' && typeof type.name === 'string') { - out.add(type.name); - } - - for (const value of Object.values(type)) { - if (Array.isArray(value)) { - value.forEach(entry => collectReferenceNames(entry, out)); - } else { - collectReferenceNames(/** @type {any} */ (value), out); - } - } -} - -/** @param {import('typedoc').Application} app */ -function load(app) { - app.options.addDeclaration({ - name: 'mintlifyDirectory', - help: 'URL prefix Mintlify serves the generated SDK pages under. Must match `sdk.directory` in docs.json.', - type: ParameterType.String, - defaultValue: 'sdk/typescript' - }); - - // Priority -1000 so this runs after the category plugin and after TypeDoc's - // own resolution: by now every type reference has a resolved name. - app.converter.on( - Converter.EVENT_RESOLVE_END, - context => { - const { project } = context; - const directory = String( - app.options.getValue('mintlifyDirectory') - ).replace(/^\/|\/$/g, ''); - - /** Documented top-level exports, by name, that Mintlify gives a page. */ - const pages = new Map(); - for (const child of project.children ?? []) { - const segment = KIND_SEGMENT[child.kind]; - if (segment) { - pages.set(child.name, `/${directory}/${segment}/${child.name}`); - } - } - - for (const reflection of project.getReflectionsByKind(LINKABLE_KINDS)) { - const names = new Set(); - collectReferenceNames(/** @type {any} */ (reflection).type, names); - - // A symbol never needs a link to its own page. - names.delete(reflection.name); - names.delete(reflection.parent?.name ?? ''); - - const links = [...names] - .filter(name => pages.has(name)) - .sort() - .map(name => `[${name}](${pages.get(name)})`); - - if (!links.length) continue; - - appendSummary(reflection, `Type: ${links.join(', ')}`); - } - }, - undefined, - -1000 - ); -} - -/** - * Append a markdown paragraph to a reflection's comment summary, creating the - * comment if the symbol is undocumented. Mintlify renders the summary as MDX, - * so the markdown becomes real anchors. - * - * @param {import('typedoc').Reflection} reflection - * @param {string} markdown - */ -function appendSummary(reflection, markdown) { - const { Comment } = require('typedoc'); - const target = /** @type {any} */ (reflection); - const comment = target.comment ?? target.signatures?.[0]?.comment; - - if (comment) { - comment.summary.push({ kind: 'text', text: `\n\n${markdown}` }); - } else { - target.comment = new Comment([{ kind: 'text', text: markdown }]); - } -} - -module.exports = { load, KIND_SEGMENT }; diff --git a/typedoc.docsv2.js b/typedoc.docsv2.js index f6f02f93..b82877bc 100644 --- a/typedoc.docsv2.js +++ b/typedoc.docsv2.js @@ -19,17 +19,6 @@ const { module.exports = { ...shared, - // Mintlify only reads a symbol's comment summary, so cross-type links have to - // be injected there as markdown. See `scripts/typedoc-plugin-mintlify.js`. - plugin: [...shared.plugin, './scripts/typedoc-plugin-mintlify.js'], - - // Must match `sdk.directory` in the docs-v2 SDK Reference tab and the - // `directory` in the docs-content-pipeline's `auth0-react-generation.js`, - // which builds the curated sidebar from this artifact. The plugin bakes - // absolute hrefs into comment summaries, so a mismatch means every type link - // 404s. - mintlifyDirectory: 'docs/sdk/auth0-react', - // Staging directory whose layout mirrors docs-v2's `main/`, so publishing is a // straight copy of two files with no path rewriting. json: './mintlify/docsv2/sdk-artifacts/auth0-react.json', From 38fd34e3164bfb5fae2c104e36b7f1d506c247ec Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw Date: Wed, 30 Sep 2026 13:58:39 +0530 Subject: [PATCH 4/4] docs(typedoc): enhance comments for clarity and readability in typedoc-plugin.js and typedoc.docsv2.js --- scripts/typedoc-plugin.js | 136 ++++++++++++++------------------------ typedoc.docsv2.js | 5 +- 2 files changed, 53 insertions(+), 88 deletions(-) diff --git a/scripts/typedoc-plugin.js b/scripts/typedoc-plugin.js index 959506ff..2ecfaa60 100644 --- a/scripts/typedoc-plugin.js +++ b/scripts/typedoc-plugin.js @@ -4,19 +4,13 @@ * * Two jobs: * - * 1. Categorize every top-level export so the landing page and sidebar read as - * "Getting Started / Hooks & HOCs / Context / Errors / Reference" instead of - * one flat alphabetical list of ~100 symbols. The symbols this SDK declares - * are tagged where they are declared; everything else is placed by two rules - * keyed on the symbol's name and declaration file. A symbol declared here - * that no rule places fails the build rather than drifting into a default - * section, which is how this SDK's own hooks ended up in "Other Types". + * 1. Categorize every top-level export into named sections instead of one flat + * alphabetical list. Own symbols carry an `@category` tag; re-exports are + * placed by rule. An own symbol that no rule places fails the build. * - * 2. Put the context interface's members directly in the sidebar, so - * `getAccessTokenSilently` or `loginWithRedirect` is one click from anywhere - * rather than "click the interface, then scan an index, then click again". - * The default theme stops the navigation tree at module level, so we extend - * DefaultTheme to add members for the entry-point interfaces only. + * 2. Put the context interface's members directly in the sidebar, so a token or + * login method is one click away. The default theme stops the nav tree at + * module level, so we extend DefaultTheme to add them. */ const { Comment, @@ -34,19 +28,15 @@ const { const ENTRY_INTERFACES = ['Auth0ContextInterface']; /** - * How a re-export is told apart from a symbol this SDK declares: its source - * file resolves inside `node_modules`. Keying on this rather than on a `src/` - * prefix is deliberate. The prefix is relative to TypeDoc's derived basePath, - * so if that anchor ever shifts (a config or layout change), own symbols stop - * matching and get silently reclassified as `Reference`, skipping the validation - * that is the whole point of the guardrail. `node_modules` is in the absolute - * path either way, so the test survives a basePath move. `ownSymbolCount` below - * is the backstop: if the set of own symbols ever empties, the build fails - * rather than passing with everything mislabelled. + * A re-export is told apart from an own symbol by whether its source resolves + * inside `node_modules`. Keyed on this rather than a `src/` prefix: the prefix + * is relative to TypeDoc's basePath, so a layout change would silently + * reclassify own symbols as `Reference` and skip validation. `ownSymbolCount` + * below is the backstop if the discriminator ever matches nothing. */ const DEPENDENCY_SOURCE_MARKER = 'node_modules'; -/** Named in diagnostics so the fix is obvious: put an `@category` in `src/`. */ +/** The practical fix named in diagnostics: put an `@category` in `src/`. */ const OWN_SOURCE_DIR = 'src/'; const SETUP = 'Getting Started'; @@ -55,17 +45,13 @@ const CONTEXT = 'Context'; const ERRORS = 'Errors'; const REFERENCE = 'Reference'; -/** - * Where a symbol lands if it escapes every rule. The validation below makes - * that unreachable, so anything showing up here is a bug in this plugin. - */ +/** Fallback if a symbol escapes every rule; validation makes it unreachable. */ const DEFAULT_CATEGORY = 'Other Types'; /** - * Section order on the landing page and in the sidebar, following the order the - * reference is read: the provider, then the hooks, then the context, then the - * errors you catch. `Reference` is last because it is reached from a signature, - * never by browsing. `*` is where any category not listed here lands. + * Section order, following how the reference is read: provider, hooks, context, + * errors. `Reference` is last (reached from a signature, not browsed); `*` is + * where any unlisted category lands. */ const CATEGORY_ORDER = [ SETUP, @@ -77,17 +63,12 @@ const CATEGORY_ORDER = [ DEFAULT_CATEGORY, ]; -/** - * The only categories a top-level export may be tagged with. Anything else is a - * typo, which would otherwise render as a plausible-looking one-entry section. - */ +/** Allowed categories for a top-level export; anything else is a typo. */ const TOP_LEVEL_CATEGORIES = [SETUP, HOOKS, CONTEXT, ERRORS, REFERENCE]; /** - * Order of the member categories on the `Auth0ContextInterface` page, and the - * only categories a member may be tagged with. Kept separate from the top-level - * set so that tagging a member with a section name, or the reverse, is caught - * rather than silently filed in the wrong place. + * Member category order and allowed set for `Auth0ContextInterface`. Kept + * separate from the top-level set so a mix-up between the two is caught. */ const MEMBER_CATEGORY_ORDER = [ 'Auth State', @@ -100,9 +81,8 @@ const MEMBER_CATEGORY_ORDER = [ ]; /** - * Context member name -> sidebar position, filled while the `@category` tags - * still exist. The renderer needs this ordering after TypeDoc's own category - * plugin has read and stripped the tags, so it cannot recompute it there. + * Context member name -> sidebar position. Captured while the `@category` tags + * exist, because the renderer runs after TypeDoc has stripped them. */ const memberSidebarOrder = new Map(); @@ -161,23 +141,20 @@ function setCategory(reflection, category) { } /** - * Decide which category a symbol belongs to. Three rules, first match wins: + * Decide a symbol's category. Three rules, first match wins: * - * 1. A tag written on a declaration in `src/`, which is kept and validated. - * Tags on a declaration outside `src/` are not rule 1: they came from a - * dependency, and rule 3 discards them. - * 2. A name ending in `Error`, which is an error class no matter who declared - * it. This precedes rule 3 because every error class is also a re-export, and - * it tests the name rather than trusting upstream's tags because upstream - * tags only one of its several error modules. - * 3. Declared outside `src/`, which makes it a supporting type reached from a - * signature, so it goes to `Reference`. This *overwrites* any category the - * symbol arrived with: TypeDoc reads `@category` out of a dependency's type - * declarations, so upstream categories turn up here whether or not we want - * them, and leaving them would strand symbols in unlisted sections. + * 1. A tag on an own symbol (source outside node_modules): kept and validated. + * 2. A name ending in `Error`: an error class whoever declared it. Precedes + * rule 3 because error classes are re-exports too, and tests the name + * because upstream tags only some of its error modules. Note this catches an + * own `FooError` left untagged before the rule-3 build error would. + * 3. A re-export: a supporting type reached from a signature, so `Reference`. + * Overwrites any inherited category (TypeDoc reads `@category` out of a + * dependency's .d.ts), which would otherwise strand it in an unlisted + * section. * - * Nothing left over is legitimate: a symbol this SDK declares and no rule places - * is the drift this plugin exists to catch, so it becomes a build error. + * An own symbol that no rule places is the drift this guards against: build + * error. * * @param {import('typedoc').DeclarationReflection} reflection * @param {string[]} allowed Categories this reflection may be tagged with. @@ -225,14 +202,9 @@ function memberCategoryIndex(reflection) { } /** - * Sidebar order for the context members: by category, then by the order they - * are declared within that category. Alphabetical would bury the ones most - * people came for under the DPoP escape hatches. - * - * Declaration order means the line the member is written on, so moving a member - * within the interface moves it in the sidebar. That holds only while every - * member of a category is declared in one file, which is true today: the auth - * state is all of `auth-state.tsx` and the rest is all of `auth0-context.tsx`. + * Sidebar order for context members: by category, then declaration line within + * it (alphabetical would bury the common ones under the DPoP escape hatches). + * Line-based ordering holds only while a category lives in one file, true today. * * @param {import('typedoc').DeclarationReflection} a * @param {import('typedoc').DeclarationReflection} b @@ -265,10 +237,9 @@ function load(app) { } } - // Backstop for the `declaredHere` test: this SDK always declares its own - // top-level exports, so a count of zero means the discriminator stopped - // recognising them and every symbol slipped into `Reference` unvalidated. - // Fail loudly rather than ship a reference with no sections. + // Backstop for `declaredHere`: this SDK always has own exports, so zero + // means the discriminator broke and everything slipped into `Reference` + // unchecked. Fail rather than ship a reference with no sections. if ((project.children?.length ?? 0) > 0 && ownSymbolCount === 0) { problems.push( 'No top-level export was recognised as declared in this SDK. The ' + @@ -296,8 +267,7 @@ function load(app) { app.logger.error(problem); } - // One summary line, not one per symbol: a jump in this count means a - // dependency added exports, which is the only thing worth noticing here. + // One summary line: a jump here means a dependency added exports. app.logger.info( `${REFERENCE} holds ${referenceCount} symbols, ` + `${reExportCount} of them re-exported from dependencies.` @@ -318,13 +288,10 @@ function load(app) { } ); - // The sidebar is built client-side and every group starts collapsed, so a - // first-time reader lands on a list of category names with nothing in sight. - // Seed the two groups people arrive for as expanded before the nav script - // runs. Reading the key first means a reader who collapses one keeps that - // choice. The key is `data-key` on the accordion, which the nav builder sets - // to the ancestor titles joined by `$`; the lowercase-dashed variant is the - // fallback derivation, seeded too so a change in either direction still works. + // Groups start collapsed client-side, so seed the two people arrive for as + // expanded. Only seed when unset, so a reader's own collapse sticks. The + // accordion key is `data-key` (ancestor titles joined by `$`); the + // lowercase-dashed form is TypeDoc's fallback, seeded too. const expandKeys = [SETUP, HOOKS].flatMap((title) => [ title, title.replace(/\s+/g, '-').toLowerCase(), @@ -352,6 +319,8 @@ function load(app) { */ function addEntryMembers(nodes, project, router) { for (const node of nodes) { + // Recurse into groups; entry-interface nodes are leaves under the current + // nav config, so member injection below only fires on leaves. if (node.children?.length) { addEntryMembers(node.children, project, router); continue; @@ -380,11 +349,9 @@ function addEntryMembers(nodes, project, router) { (memberSidebarOrder.get(b.name) ?? Number.MAX_SAFE_INTEGER) ); - // Ask the router for the href. TypeDoc 0.28 moved URL assignment out of the - // reflections and behind the Router, so `member.url` and `member.anchor` are - // both undefined here: building the path by hand produced links reading - // `docs/undefined#isLoading`. `getFullUrl` is what TypeDoc's own frontend - // uses for nav entries, and it already includes the anchor. + // TypeDoc 0.28 moved URLs behind the Router, so `member.url`/`.anchor` are + // undefined here. `getFullUrl` (what the frontend uses for nav) includes + // the anchor. const links = members .filter((member) => router.hasUrl(member)) .map((member) => ({ @@ -401,9 +368,8 @@ function addEntryMembers(nodes, project, router) { } /** - * `categoryOrder` is a single global setting, so it has to cover both the - * top-level export categories and the context interface's member categories. - * The two sets are disjoint, so concatenating them orders each page correctly. + * `categoryOrder` is one global setting covering both top-level and member + * categories. The sets are disjoint, so concatenating orders each page right. */ const ALL_CATEGORY_ORDER = [...MEMBER_CATEGORY_ORDER, ...CATEGORY_ORDER]; diff --git a/typedoc.docsv2.js b/typedoc.docsv2.js index b82877bc..831c5410 100644 --- a/typedoc.docsv2.js +++ b/typedoc.docsv2.js @@ -5,9 +5,8 @@ // only the JSON artifact that Mintlify consumes. // // `out`, `cleanOutputDir`, `theme` and `customCss` are dropped deliberately. -// TypeDoc's CLI renders HTML whenever `out` is set, even alongside `--json` -// (`if (!json || app.options.isSet("out"))`), so leaving it in would rebuild -// `docs/` as a side effect of building the Mintlify artifact. +// TypeDoc emits an HTML output for `out` independently of `--json`, so leaving +// it in would rebuild `docs/` as a side effect of building the artifact. const { out, cleanOutputDir,