Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All@@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
85 changes: 82 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
*/
function errorsToParsedErrors(error: unknown): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

parsedErrors.raw.push(...error.errors);

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
default:
parsedErrors.global.push(error);
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':// Error 1: "First name is required"// Error 2: "First name contains invalid characters"// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';-function useClerkSignal(signal: 'signIn') {+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
…
}
-export function useSignInSignal() {+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
📝 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
functionuseClerkSignal(signal: 'signIn'){
// Add the type import
importtype{StateasClerkState}from'@clerk/types';
functionuseClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']>|null{
}
exportfunctionuseSignInSignal(): ReturnType<ClerkState['signInSignal']>|null{
returnuseClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down