Skip to content
Open
2 changes: 1 addition & 1 deletion .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-check
// @ts-check - Enable TypeScript checks for safer MDX post-processing and link rewriting
import { MarkdownPageEvent } from 'typedoc-plugin-markdown';

/**
Expand Down
32 changes: 29 additions & 3 deletions .typedoc/custom-theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {

this.partials = {
...superPartials,
/**
* This hides the "Experimental" text and "Example" section from the output (by default).
* @param {import('typedoc').Comment} model
* @param {{ headingLevel?: number; showSummary?: boolean; showTags?: boolean; showReturns?: boolean; isTableColumn?: boolean }} [options]
*/
comment: (model, options) => {
if (
model.hasModifier('@experimental') &&
[ReflectionKind.Class, ReflectionKind.Interface].includes(this.page?.model?.kind)
) {
model.removeModifier('@experimental');
model.removeTags('@example');
model.removeTags('@see');

const res = superPartials.comment(model, options);

return res.replace(/^\n+/, '');
}
return superPartials.comment(model, options);
},
/**
* This hides the "Type parameters" section and the signature title from the output (by default). Shows the signature title if the `@displayFunctionSignature` tag is present.
* @param {import('typedoc').SignatureReflection} model
Expand Down Expand Up @@ -218,13 +238,19 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
const customizedModel = model;
customizedModel.typeParameters = undefined;

Comment on lines 238 to 240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid mutating typeParameters; restore after rendering.

You unset typeParameters and never restore it, which may affect downstream partials that read the same model instance.

-        const customizedModel = model;
-        customizedModel.typeParameters = undefined;
+        const customizedModel = model;
+        const originalTypeParameters = customizedModel.typeParameters;
+        customizedModel.typeParameters = undefined;
@@
-        customizedModel.groups = originalGroups;
+        customizedModel.groups = originalGroups;
+        customizedModel.typeParameters = originalTypeParameters;

Also applies to: 254-255

🤖 Prompt for AI Agents
In .typedoc/custom-theme.mjs around lines 238-240 (and likewise lines 254-255),
the code mutates model.typeParameters and never restores it; instead create a
shallow copy of the model (or save the original typeParameters to a temp
variable) before clearing typeParameters, use the copy/temp for rendering, and
then restore the original model.typeParameters after rendering so downstream
partials see the original data.

// Extract the Accessors group (if any) and prevent default rendering for it
const originalGroups = customizedModel.groups;
const accessorsGroup = originalGroups?.find(g => g.title === 'Accessors');
const groupsWithoutAccessors = originalGroups?.filter(g => g.title !== 'Accessors');

// When an interface extends another interface, typedoc will generate a "Methods" group
// We want to hide this group from being rendered
const groupsWithoutMethods = originalGroups?.filter(g => g.title !== 'Methods');

// Extract the Accessors group (if any) and prevent default rendering for it
const accessorsGroup = groupsWithoutMethods?.find(g => g.title === 'Accessors');
const groupsWithoutAccessors = groupsWithoutMethods?.filter(g => g.title !== 'Accessors');
Comment on lines +243 to +249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Hiding every “Methods” group is too broad; restrict to interfaces (extends case).

As written, you’ll also remove legitimate methods from classes. Limit the filter to interface pages only.

-        const groupsWithoutMethods = originalGroups?.filter(g => g.title !== 'Methods');
+        const isInterface = customizedModel?.kind === ReflectionKind.Interface;
+        const groupsWithoutMethods = isInterface
+          ? originalGroups?.filter(g => g.title !== 'Methods')
+          : originalGroups;

Build a sample class with methods in the docs to confirm the Methods section remains visible.


🏁 Script executed:

#!/bin/bash
# Locate ReflectionKind usage and import in custom-theme.mjs
grep -R "ReflectionKind" -n .typedoc/custom-theme.mjs || true
# Locate where customizedModel is defined or passed in
grep -R "customizedModel" -n .typedoc/custom-theme.mjs || true

Length of output: 1133


Restrict “Methods” filtering to interfaces only
Only omit the “Methods” group when customizedModel.kind === ReflectionKind.Interface; leave it intact for classes.

-        const groupsWithoutMethods = originalGroups?.filter(g => g.title !== 'Methods');
+        const isInterface = customizedModel.kind === ReflectionKind.Interface;
+        const groupsWithoutMethods = isInterface
+          ? originalGroups.filter(g => g.title !== 'Methods')
+          : originalGroups;

[matches .typedoc/custom-theme.mjs:243]

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// When an interface extends another interface, typedoc will generate a "Methods" group
// We want to hide this group from being rendered
const groupsWithoutMethods = originalGroups?.filter(g => g.title !== 'Methods');
// Extract the Accessors group (if any) and prevent default rendering for it
const accessorsGroup = groupsWithoutMethods?.find(g => g.title === 'Accessors');
const groupsWithoutAccessors = groupsWithoutMethods?.filter(g => g.title !== 'Accessors');
// When an interface extends another interface, typedoc will generate a "Methods" group
// We want to hide this group from being rendered
const isInterface = customizedModel.kind === ReflectionKind.Interface;
const groupsWithoutMethods = isInterface
? originalGroups.filter(g => g.title !== 'Methods')
: originalGroups;
// Extract the Accessors group (if any) and prevent default rendering for it
const accessorsGroup = groupsWithoutMethods?.find(g => g.title === 'Accessors');
const groupsWithoutAccessors = groupsWithoutMethods?.filter(g => g.title !== 'Accessors');
🤖 Prompt for AI Agents
In .typedoc/custom-theme.mjs around lines 243 to 249, the current code always
filters out the "Methods" group; change this to only filter when the model is an
interface by wrapping the "Methods" removal in a conditional that checks if
customizedModel.kind === ReflectionKind.Interface (ensure ReflectionKind is
imported/available in this module). Leave the "Accessors" extraction and
subsequent filtering unchanged so accessors are still handled the same way.


customizedModel.groups = groupsWithoutAccessors;
const nonAccessorOutput = superPartials.memberWithGroups(customizedModel, options);

customizedModel.groups = originalGroups;

/** @type {string[]} */
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows",
"test:integration:astro": "E2E_APP_ID=astro.* pnpm test:integration:base --grep @astro",
"test:integration:base": "pnpm playwright test --config integration/playwright.config.ts",
"test:integration:billing": "E2E_APP_ID=withBilling.* pnpm test:integration:base --grep @billing",
"test:integration:billing": "E2E_APP_ID=withBilling.next.appRouter pnpm test:integration:base --grep @billing",
"test:integration:cleanup": "pnpm playwright test --config integration/playwright.cleanup.config.ts",
"test:integration:custom": "pnpm test:integration:base --grep @custom",
"test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts",
Expand All @@ -53,6 +53,7 @@
"test:integration:tanstack-react-start": "E2E_APP_ID=tanstack.react-start pnpm test:integration:base --grep @tanstack-react-start",
"test:integration:vue": "E2E_APP_ID=vue.vite pnpm test:integration:base --grep @vue",
"test:typedoc": "pnpm typedoc:generate && cd ./.typedoc && vitest run",
"test:typedoc:update": "pnpm typedoc:generate && cd ./.typedoc && vitest --u",
"turbo:clean": "turbo daemon clean",
"typedoc:generate": "pnpm build:declarations && pnpm typedoc:generate:skip-build",
"typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && rm -rf .typedoc/docs && mv .typedoc/temp-docs .typedoc/docs",
Expand Down
108 changes: 108 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/README-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Snapshot Testing for Clerk Resources

This directory contains snapshot tests for Clerk resource classes using Vitest. Snapshot tests help ensure that the structure and serialization of resource objects remain consistent over time.

## What are Snapshot Tests?

Snapshot tests capture the output of a function or object and store it as a "snapshot" file. When the test runs again, the current output is compared against the stored snapshot. If they differ, the test fails, alerting you to potential breaking changes.

## How to Add Snapshot Tests

### Basic Pattern

```typescript
describe('ResourceName Snapshots', () => {
it('should match snapshot for resource instance structure', () => {
const resource = new ResourceName({
// Provide test data that represents a typical resource
id: 'test_123',
name: 'Test Resource',
// ... other properties
});

const snapshot = {
id: resource.id,
name: resource.name,
// Include relevant public properties
};

expect(snapshot).toMatchSnapshot();
});
});
```

### Testing Different States

```typescript
it('should match snapshot for empty/null state', () => {
const resource = new ResourceName({
id: 'empty_test',
name: null,
// ... other properties with null/empty values
} as any); // Use 'as any' if TypeScript complains about null values

expect({
id: resource.id,
name: resource.name,
}).toMatchSnapshot();
});
```

### Testing Serialization Methods

For resources with `__internal_toSnapshot()` methods:

```typescript
it('should match snapshot for __internal_toSnapshot method', () => {
const resource = new ResourceName(testData);
expect(resource.__internal_toSnapshot()).toMatchSnapshot();
});
```

## Best Practices

1. **Use Fixed Dates**: Use `vi.useFakeTimers()` and `vi.setSystemTime()` to ensure consistent timestamps in snapshots.

2. **Include Relevant Properties**: Focus on public API properties that consumers rely on, not internal implementation details.

3. **Test Edge Cases**: Include tests for null values, empty states, and different configurations.

4. **Keep Snapshots Small**: Focus on the essential structure rather than including every property.

5. **Update When Intentional**: When you intentionally change a resource's structure, update the snapshots using `npm test -- --update-snapshots`.

## Running Snapshot Tests

```bash
# Run all resource tests
npm test -- src/core/resources/__tests__/*.spec.ts

# Run specific test file
npm test -- src/core/resources/__tests__/Client.spec.ts

# Update snapshots when structure changes intentionally
npm test -- --update-snapshots
```

## Examples

See the following files for examples:

- `Client.spec.ts` - Complex resource with nested objects
- `Environment.spec.ts` - Resource with configuration objects
- `Image.spec.ts` - Simple resource with basic properties

## When Snapshots Fail

When a snapshot test fails:

1. **Review the diff** to understand what changed
2. **Determine if the change is intentional**:
- If yes: Update the snapshot with `--update-snapshots`
- If no: Fix the code to maintain backward compatibility
3. **Consider the impact** on API consumers
4. **Update documentation** if the public API changed

## Snapshot Files

Snapshot files are stored in `__snapshots__/` directories and should be committed to version control. They serve as documentation of your resource structures and help catch unintended changes.
1 change: 1 addition & 0 deletions typedoc.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const typedocPluginMarkdownOptions = {
hideModifiers: true,
hideDefaults: true,
hideInherited: true,
hideOverrides: true,
},
fileExtension: '.mdx',
excludeScopesInPaths: true,
Expand Down
Loading