r3ui-lib 1.2.31 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/login.css +1 -1
  3. package/dist/login.es.js +1441 -1396
  4. package/dist/login.umd.js +5 -5
  5. package/dist/r3ui-lib.css +1 -1
  6. package/dist/r3ui-lib.es.js +7883 -7783
  7. package/dist/r3ui-lib.umd.js +30 -30
  8. package/dist/types/components/SsoLoginScreen.vue.d.ts.map +1 -1
  9. package/dist/types/components/dropdowns/DefaultDropDown.vue.d.ts.map +1 -1
  10. package/dist/types/components/tables/Table.vue.d.ts +2 -0
  11. package/dist/types/components/tables/Table.vue.d.ts.map +1 -1
  12. package/dist/types/composables/table/useRemoteTable.d.ts +6 -1
  13. package/dist/types/composables/table/useRemoteTable.d.ts.map +1 -1
  14. package/dist/types/main.d.ts +1 -1
  15. package/dist/types/main.d.ts.map +1 -1
  16. package/dist/types/utils/auth/exchangePasswordToken.d.ts.map +1 -1
  17. package/dist/types/utils/auth/handlePkceCallback.d.ts.map +1 -1
  18. package/dist/types/utils/auth/logout.d.ts +1 -1
  19. package/dist/types/utils/auth/logout.d.ts.map +1 -1
  20. package/dist/types/utils/auth/runLogoutHandler.d.ts.map +1 -1
  21. package/dist/types/utils/auth/signInWithSSO.d.ts +1 -0
  22. package/dist/types/utils/auth/signInWithSSO.d.ts.map +1 -1
  23. package/dist/types/utils/tableAjaxRequest.d.ts +1 -0
  24. package/dist/types/utils/tableAjaxRequest.d.ts.map +1 -1
  25. package/dist/types/utils/upstreamHttpStatus.d.ts +9 -0
  26. package/dist/types/utils/upstreamHttpStatus.d.ts.map +1 -0
  27. package/nuxt-errors/app/error.vue +85 -0
  28. package/nuxt-errors/composables/presentPageError.ts +21 -0
  29. package/nuxt-errors/error.vue +85 -0
  30. package/nuxt-errors/nuxt.config.ts +6 -0
  31. package/nuxt-errors/plugins/app-error-log.client.ts +21 -0
  32. package/nuxt-errors/utils/classifyTransport.ts +56 -0
  33. package/nuxt-errors/utils/cloneJson.ts +17 -0
  34. package/nuxt-errors/utils/extractHttpStatus.ts +51 -0
  35. package/nuxt-errors/utils/extractLaravel.ts +73 -0
  36. package/nuxt-errors/utils/extractRequestId.ts +53 -0
  37. package/nuxt-errors/utils/isHttpStatus.ts +3 -0
  38. package/nuxt-errors/utils/logAppError.ts +68 -0
  39. package/nuxt-errors/utils/normalizeAppError.ts +178 -0
  40. package/nuxt-errors/utils/presentAppError.ts +115 -0
  41. package/nuxt-errors/utils/publicHttpMessage.ts +23 -0
  42. package/nuxt-errors/utils/serializeCause.ts +25 -0
  43. package/nuxt-errors/utils/types.ts +55 -0
  44. package/package.json +4 -1
@@ -0,0 +1,53 @@
1
+ import { REQUEST_ID_HEADERS } from './types';
2
+
3
+ function readHeader(
4
+ headers: unknown,
5
+ name: string
6
+ ): string | undefined {
7
+ if (!headers || typeof headers !== 'object') {
8
+ return undefined;
9
+ }
10
+
11
+ const rec = headers as {
12
+ get?: (key: string) => unknown;
13
+ [key: string]: unknown;
14
+ };
15
+
16
+ if (typeof rec.get === 'function') {
17
+ const value = rec.get(name) ?? rec.get(name.toLowerCase());
18
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
19
+ }
20
+
21
+ const direct = rec[name] ?? rec[name.toLowerCase()];
22
+ if (typeof direct === 'string' && direct.trim()) {
23
+ return direct.trim();
24
+ }
25
+ if (Array.isArray(direct) && typeof direct[0] === 'string' && direct[0].trim()) {
26
+ return direct[0].trim();
27
+ }
28
+
29
+ return undefined;
30
+ }
31
+
32
+ export function extractRequestId(value: unknown): string | undefined {
33
+ if (!value || typeof value !== 'object') {
34
+ return undefined;
35
+ }
36
+
37
+ const rec = value as {
38
+ headers?: unknown;
39
+ response?: { headers?: unknown };
40
+ };
41
+
42
+ const sources = [rec.headers, rec.response?.headers];
43
+ for (const headers of sources) {
44
+ for (const name of REQUEST_ID_HEADERS) {
45
+ const id = readHeader(headers, name);
46
+ if (id) {
47
+ return id;
48
+ }
49
+ }
50
+ }
51
+
52
+ return undefined;
53
+ }
@@ -0,0 +1,3 @@
1
+ export function isHttpStatus(value: unknown): value is number {
2
+ return Number.isInteger(value) && Number(value) >= 400 && Number(value) < 600;
3
+ }
@@ -0,0 +1,68 @@
1
+ import { normalizeAppError } from './normalizeAppError';
2
+ import { serializeCause } from './serializeCause';
3
+
4
+ export type AppErrorLogEntry = {
5
+ statusCode: number;
6
+ kind: string;
7
+ message: string;
8
+ requestId?: string;
9
+ stack?: string;
10
+ cause?: unknown;
11
+ };
12
+
13
+ const loggedErrors = new WeakSet<object>();
14
+
15
+ export function resetLoggedErrorsForTests(): void {
16
+ // WeakSet нельзя очистить; тесты используют новые объекты.
17
+ }
18
+
19
+ export function wasLogged(error: object): boolean {
20
+ return loggedErrors.has(error);
21
+ }
22
+
23
+ export function markLogged(error: unknown): void {
24
+ if (error && typeof error === 'object') {
25
+ loggedErrors.add(error);
26
+ }
27
+ }
28
+
29
+ export function toAppErrorLogEntry(
30
+ error: unknown,
31
+ options: { isDev: boolean }
32
+ ): AppErrorLogEntry {
33
+ const appError = normalizeAppError(error);
34
+ if (options.isDev) {
35
+ return {
36
+ statusCode: appError.statusCode,
37
+ kind: appError.kind,
38
+ message: appError.message ?? appError.messageKey,
39
+ requestId: appError.requestId,
40
+ stack: appError.stack,
41
+ cause: serializeCause(appError.cause),
42
+ };
43
+ }
44
+
45
+ return {
46
+ statusCode: appError.statusCode,
47
+ kind: appError.kind,
48
+ message: appError.messageKey,
49
+ requestId: appError.requestId,
50
+ };
51
+ }
52
+
53
+ export function logAppError(
54
+ error: unknown,
55
+ deps: {
56
+ isDev: boolean;
57
+ logger?: Pick<Console, 'error'>;
58
+ }
59
+ ): void {
60
+ if (error && typeof error === 'object' && loggedErrors.has(error)) {
61
+ return;
62
+ }
63
+
64
+ markLogged(error);
65
+ const entry = toAppErrorLogEntry(error, { isDev: deps.isDev });
66
+ const logger = deps.logger ?? console;
67
+ logger.error('[app-error]', entry);
68
+ }
@@ -0,0 +1,178 @@
1
+ import { extractHttpStatus } from './extractHttpStatus';
2
+ import { extractFieldErrors, extractPublicMessage } from './extractLaravel';
3
+ import { extractRequestId } from './extractRequestId';
4
+ import {
5
+ isAbortError,
6
+ isIntentionalAbort,
7
+ isNetworkError,
8
+ isTimeoutError,
9
+ } from './classifyTransport';
10
+ import { MESSAGE_KEYS, type AppError, type AppErrorKind, type NormalizeAppErrorContext } from './types';
11
+
12
+ function kindFromStatus(status: number): AppErrorKind {
13
+ if (status === 401) return 'unauthorized';
14
+ if (status === 403) return 'forbidden';
15
+ if (status === 404) return 'not_found';
16
+ if (status === 409) return 'conflict';
17
+ if (status === 422) return 'validation';
18
+ if (status === 429) return 'rate_limit';
19
+ if (status === 408 || status === 504) return 'timeout';
20
+ if (status === 499) return 'abort';
21
+ if (status === 503) return 'network';
22
+ if (status >= 500) return 'server';
23
+ return 'client';
24
+ }
25
+
26
+ function readPayload(value: unknown, seen: Set<unknown> = new Set()): unknown {
27
+ if (!value || typeof value !== 'object' || seen.has(value)) {
28
+ return value;
29
+ }
30
+ seen.add(value);
31
+ const rec = value as { data?: unknown; cause?: unknown };
32
+ if (rec.data !== undefined) {
33
+ return rec.data;
34
+ }
35
+ if ('cause' in rec) {
36
+ return readPayload(rec.cause, seen);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function readStack(value: unknown): string | undefined {
42
+ if (value instanceof Error && value.stack) {
43
+ return value.stack;
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ function isAllowedPublicStatus(status: number): boolean {
49
+ return status === 400 || status === 403 || status === 404 || status === 409 || status === 422 || status === 429;
50
+ }
51
+
52
+ function isMalformedPayload(value: unknown): boolean {
53
+ if (!value || typeof value !== 'object') {
54
+ return false;
55
+ }
56
+
57
+ const rec = value as { kind?: unknown; name?: unknown; message?: unknown };
58
+ if (rec.kind === 'malformed') {
59
+ return true;
60
+ }
61
+ if (rec.name === 'SyntaxError') {
62
+ return true;
63
+ }
64
+ return typeof rec.message === 'string' && /unexpected token|is not valid json|malformed/i.test(rec.message);
65
+ }
66
+
67
+ /**
68
+ * Сырой Response нужно клонировать снаружи, если caller ещё читает body.
69
+ * Здесь status берётся синхронно; body ожидается уже в `data` (FetchError/ofetch).
70
+ */
71
+ export function normalizeAppError(
72
+ value: unknown,
73
+ context: NormalizeAppErrorContext = {}
74
+ ): AppError {
75
+ const cause = value;
76
+ const requestId = extractRequestId(value);
77
+ const payload = readPayload(value);
78
+ const fieldErrors = extractFieldErrors(payload);
79
+ const httpStatus = extractHttpStatus(value);
80
+
81
+ if (context.intentionalAbort || isIntentionalAbort(value)) {
82
+ return {
83
+ kind: 'abort',
84
+ statusCode: 499,
85
+ messageKey: MESSAGE_KEYS.abort,
86
+ requestId,
87
+ cause,
88
+ stack: readStack(value),
89
+ };
90
+ }
91
+
92
+ if (isAbortError(value) && httpStatus === undefined) {
93
+ return {
94
+ kind: 'abort',
95
+ statusCode: 499,
96
+ messageKey: MESSAGE_KEYS.abort,
97
+ requestId,
98
+ cause,
99
+ stack: readStack(value),
100
+ };
101
+ }
102
+
103
+ if (isTimeoutError(value) && httpStatus === undefined) {
104
+ return {
105
+ kind: 'timeout',
106
+ statusCode: 504,
107
+ messageKey: MESSAGE_KEYS.timeout,
108
+ requestId,
109
+ cause,
110
+ stack: readStack(value),
111
+ };
112
+ }
113
+
114
+ if (isNetworkError(value) && httpStatus === undefined) {
115
+ return {
116
+ kind: 'network',
117
+ statusCode: 503,
118
+ messageKey: MESSAGE_KEYS.network,
119
+ requestId,
120
+ cause,
121
+ stack: readStack(value),
122
+ };
123
+ }
124
+
125
+ if (httpStatus !== undefined) {
126
+ const kind = kindFromStatus(httpStatus);
127
+ const publicMessage = isAllowedPublicStatus(httpStatus)
128
+ ? extractPublicMessage(payload)
129
+ : undefined;
130
+
131
+ return {
132
+ kind,
133
+ statusCode: httpStatus,
134
+ messageKey: MESSAGE_KEYS[kind],
135
+ message: publicMessage,
136
+ fieldErrors,
137
+ requestId,
138
+ cause,
139
+ stack: readStack(value),
140
+ };
141
+ }
142
+
143
+ if (isMalformedPayload(value)) {
144
+ return {
145
+ kind: 'malformed',
146
+ statusCode: 502,
147
+ messageKey: MESSAGE_KEYS.malformed,
148
+ requestId,
149
+ cause,
150
+ stack: readStack(value),
151
+ };
152
+ }
153
+
154
+ if (typeof value === 'string' && value.trim()) {
155
+ return {
156
+ kind: 'runtime',
157
+ statusCode: 500,
158
+ messageKey: MESSAGE_KEYS.runtime,
159
+ requestId,
160
+ cause,
161
+ stack: undefined,
162
+ };
163
+ }
164
+
165
+ return {
166
+ kind: 'runtime',
167
+ statusCode: 500,
168
+ messageKey: MESSAGE_KEYS.runtime,
169
+ fieldErrors,
170
+ requestId,
171
+ cause,
172
+ stack: readStack(value),
173
+ };
174
+ }
175
+
176
+ export function isUnauthorizedError(error: unknown): boolean {
177
+ return normalizeAppError(error).statusCode === 401;
178
+ }
@@ -0,0 +1,115 @@
1
+ import { isIntentionalAbort } from './classifyTransport';
2
+ import { normalizeAppError } from './normalizeAppError';
3
+ import type { AppError, AppErrorPresentation } from './types';
4
+
5
+ export type FatalErrorPayload = {
6
+ statusCode: number;
7
+ statusMessage: string;
8
+ message: string;
9
+ fatal: boolean;
10
+ data: {
11
+ kind: AppError['kind'];
12
+ messageKey: string;
13
+ requestId?: string;
14
+ fieldErrors?: Record<string, string[]>;
15
+ stack?: string;
16
+ cause?: unknown;
17
+ };
18
+ };
19
+
20
+ export type PresentFatalErrorDeps = {
21
+ showError: (payload: FatalErrorPayload) => void;
22
+ isDev: boolean;
23
+ serializeCause: (cause: unknown) => unknown;
24
+ };
25
+
26
+ export function toFatalErrorPayload(
27
+ error: unknown,
28
+ deps: { isDev: boolean; serializeCause: (cause: unknown) => unknown }
29
+ ): FatalErrorPayload {
30
+ const appError = normalizeAppError(error);
31
+ return {
32
+ statusCode: appError.statusCode,
33
+ statusMessage: appError.message ?? appError.messageKey,
34
+ message: appError.message ?? appError.messageKey,
35
+ fatal: true,
36
+ data: {
37
+ kind: appError.kind,
38
+ messageKey: appError.messageKey,
39
+ requestId: appError.requestId,
40
+ fieldErrors: appError.fieldErrors,
41
+ stack: deps.isDev ? appError.stack : undefined,
42
+ cause: deps.isDev ? deps.serializeCause(appError.cause) : undefined,
43
+ },
44
+ };
45
+ }
46
+
47
+ export function presentFatalError(
48
+ error: unknown,
49
+ deps: PresentFatalErrorDeps
50
+ ): void {
51
+ deps.showError(toFatalErrorPayload(error, deps));
52
+ }
53
+
54
+ export type FormErrorView = {
55
+ statusCode: number;
56
+ kind: AppError['kind'];
57
+ messageKey: string;
58
+ message: string;
59
+ fieldErrors: Record<string, string[]>;
60
+ };
61
+
62
+ export function toFormErrors(error: unknown): FormErrorView {
63
+ const appError = normalizeAppError(error);
64
+ return {
65
+ statusCode: appError.statusCode,
66
+ kind: appError.kind,
67
+ messageKey: appError.messageKey,
68
+ message: appError.message ?? appError.messageKey,
69
+ fieldErrors: appError.fieldErrors ?? {},
70
+ };
71
+ }
72
+
73
+ export function shouldSkipUi(error: unknown, presentation?: AppErrorPresentation): boolean {
74
+ if (presentation === 'silent') {
75
+ return true;
76
+ }
77
+ return isIntentionalAbort(error);
78
+ }
79
+
80
+ export type ErrorPageView = {
81
+ statusCode: number;
82
+ messageKey: string;
83
+ publicMessage?: string;
84
+ showRetry: boolean;
85
+ showHome: boolean;
86
+ };
87
+
88
+ export function toErrorPageView(error: unknown): ErrorPageView {
89
+ const rec = error as {
90
+ statusCode?: unknown;
91
+ data?: { messageKey?: unknown };
92
+ message?: unknown;
93
+ statusMessage?: unknown;
94
+ };
95
+ const normalized = normalizeAppError(error);
96
+ const statusCode =
97
+ typeof rec?.statusCode === 'number' ? rec.statusCode : normalized.statusCode;
98
+ const messageKey =
99
+ typeof rec?.data?.messageKey === 'string'
100
+ ? rec.data.messageKey
101
+ : normalized.messageKey;
102
+ const showRetry =
103
+ statusCode >= 500 ||
104
+ normalized.kind === 'network' ||
105
+ normalized.kind === 'timeout' ||
106
+ normalized.kind === 'malformed';
107
+
108
+ return {
109
+ statusCode,
110
+ messageKey,
111
+ publicMessage: normalized.message,
112
+ showRetry,
113
+ showHome: true,
114
+ };
115
+ }
@@ -0,0 +1,23 @@
1
+ import { extractHttpStatus } from './extractHttpStatus';
2
+
3
+ export function publicHttpMessage(statusCode: number): string {
4
+ if (statusCode === 400) return 'Bad Request';
5
+ if (statusCode === 401) return 'Unauthorized';
6
+ if (statusCode === 403) return 'Forbidden';
7
+ if (statusCode === 404) return 'Not Found';
8
+ if (statusCode === 409) return 'Conflict';
9
+ if (statusCode === 422) return 'Unprocessable Entity';
10
+ if (statusCode === 429) return 'Too Many Requests';
11
+ if (statusCode === 502) return 'Bad Gateway';
12
+ if (statusCode === 503) return 'Service Unavailable';
13
+ if (statusCode === 504) return 'Gateway Timeout';
14
+ if (statusCode >= 500) return 'Internal Server Error';
15
+ return 'Request failed';
16
+ }
17
+
18
+ export function statusFromUnknown(
19
+ error: unknown,
20
+ fallback = 500
21
+ ): number {
22
+ return extractHttpStatus(error) ?? fallback;
23
+ }
@@ -0,0 +1,25 @@
1
+ import { cloneJson } from './cloneJson';
2
+
3
+ export function serializeCause(cause: unknown): unknown {
4
+ if (cause instanceof Error) {
5
+ const extra = cause as Error & {
6
+ status?: unknown;
7
+ statusCode?: unknown;
8
+ data?: unknown;
9
+ };
10
+ return {
11
+ name: extra.name,
12
+ message: extra.message,
13
+ stack: extra.stack,
14
+ status: extra.status,
15
+ statusCode: extra.statusCode,
16
+ data: cloneJson(extra.data),
17
+ };
18
+ }
19
+
20
+ if (typeof cause === 'object' && cause !== null) {
21
+ return cloneJson(cause);
22
+ }
23
+
24
+ return cause;
25
+ }
@@ -0,0 +1,55 @@
1
+ export type AppErrorKind =
2
+ | 'client'
3
+ | 'unauthorized'
4
+ | 'forbidden'
5
+ | 'not_found'
6
+ | 'conflict'
7
+ | 'validation'
8
+ | 'rate_limit'
9
+ | 'server'
10
+ | 'network'
11
+ | 'timeout'
12
+ | 'abort'
13
+ | 'malformed'
14
+ | 'runtime';
15
+
16
+ export type AppErrorPresentation = 'fatal' | 'local' | 'silent';
17
+
18
+ export type AppError = {
19
+ kind: AppErrorKind;
20
+ statusCode: number;
21
+ messageKey: string;
22
+ /** Публичный текст только для ожидаемых 4xx. */
23
+ message?: string;
24
+ fieldErrors?: Record<string, string[]>;
25
+ requestId?: string;
26
+ cause?: unknown;
27
+ stack?: string;
28
+ };
29
+
30
+ export type NormalizeAppErrorContext = {
31
+ presentation?: AppErrorPresentation;
32
+ /** Явная отмена запроса — UI можно не показывать. */
33
+ intentionalAbort?: boolean;
34
+ };
35
+
36
+ export const REQUEST_ID_HEADERS = [
37
+ 'x-request-id',
38
+ 'x-correlation-id',
39
+ ] as const;
40
+
41
+ export const MESSAGE_KEYS: Record<AppErrorKind, string> = {
42
+ client: 'pages.errors.bad_request',
43
+ unauthorized: 'pages.errors.unauthorized',
44
+ forbidden: 'pages.errors.forbidden',
45
+ not_found: 'pages.errors.not_found',
46
+ conflict: 'pages.errors.conflict',
47
+ validation: 'pages.errors.validation',
48
+ rate_limit: 'pages.errors.rate_limit',
49
+ server: 'pages.errors.server',
50
+ network: 'pages.errors.network',
51
+ timeout: 'pages.errors.timeout',
52
+ abort: 'pages.errors.abort',
53
+ malformed: 'pages.errors.malformed',
54
+ runtime: 'pages.errors.unexpected',
55
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "r3ui-lib",
3
3
  "private": false,
4
- "version": "1.2.31",
4
+ "version": "1.3.0",
5
5
  "description": "Modern Vue 3 UI component library with TypeScript support",
6
6
  "keywords": [
7
7
  "vue",
@@ -37,12 +37,15 @@
37
37
  "import": "./dist/login.es.js",
38
38
  "require": "./dist/login.umd.js"
39
39
  },
40
+ "./nuxt-errors": "./nuxt-errors/nuxt.config.ts",
41
+ "./nuxt-errors/*": "./nuxt-errors/*",
40
42
  "./styles": "./dist/r3ui-lib.css",
41
43
  "./dist/r3ui-lib.css": "./dist/r3ui-lib.css"
42
44
  },
43
45
  "files": [
44
46
  "dist",
45
47
  "assets",
48
+ "nuxt-errors",
46
49
  "README.md",
47
50
  "LICENSE",
48
51
  "CHANGELOG.md"