vanilla-jui 1.7.0 → 1.7.2
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.
- package/dist/index.d.ts +39 -20
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/accordion.ts +6 -12
- package/src/components/flow.ts +17 -26
- package/src/components/form.ts +11 -28
- package/src/components/menu.ts +7 -13
- package/src/components/modal.ts +40 -26
- package/src/components/offcanvas.ts +6 -12
- package/src/components/pagination.ts +10 -24
- package/src/components/swiper.ts +7 -13
- package/src/components/tabs.ts +6 -12
- package/src/components/toc.ts +5 -8
- package/src/css/themes/default/menu.css +12 -6
- package/src/index.ts +4 -1
- package/src/primitives/drop.ts +5 -8
- package/src/primitives/parabola.ts +6 -12
- package/src/primitives/popup.ts +3 -3
- package/src/primitives/sticky.ts +3 -3
- package/src/primitives/theme.ts +29 -14
- package/src/primitives/toast.ts +17 -12
- package/src/primitives/tooltip.ts +5 -19
- package/src/utilities/config.ts +158 -0
- package/src/utilities/id.ts +121 -2
- package/src/utilities/merge.ts +97 -0
- package/src/utilities/types.ts +3 -82
- package/src/utilities/validator.ts +6 -17
package/src/primitives/drop.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from '../utilities/dom.ts';
|
|
9
9
|
import { createEventManager } from '../utilities/events.ts';
|
|
10
10
|
import { randomId } from '../utilities/id.ts';
|
|
11
|
-
import { type
|
|
11
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
12
12
|
import { createLoading } from './loading.ts';
|
|
13
13
|
|
|
14
14
|
type DropMode = 'hover' | 'click';
|
|
@@ -144,11 +144,8 @@ const DROP_PROPS_SCHEMA = {
|
|
|
144
144
|
},
|
|
145
145
|
className: {
|
|
146
146
|
default: DEFAULT_CLASS_NAMES,
|
|
147
|
-
type: '
|
|
148
|
-
|
|
149
|
-
...DEFAULT_CLASS_NAMES,
|
|
150
|
-
...(value && typeof value === 'object' ? value : {}),
|
|
151
|
-
}),
|
|
147
|
+
type: 'plainObject',
|
|
148
|
+
merge: 'shallow',
|
|
152
149
|
},
|
|
153
150
|
id: {
|
|
154
151
|
default: null,
|
|
@@ -166,10 +163,10 @@ const DROP_PROPS_SCHEMA = {
|
|
|
166
163
|
hoverIntent: { default: true, type: 'boolean' },
|
|
167
164
|
onShown: { default: null, types: ['function', 'null'] },
|
|
168
165
|
onHidden: { default: null, types: ['function', 'null'] },
|
|
169
|
-
} satisfies
|
|
166
|
+
} satisfies ConfigSchema<DropProps>;
|
|
170
167
|
|
|
171
168
|
function normalizeProps(input: DropProps): ResolvedDropProps {
|
|
172
|
-
const props =
|
|
169
|
+
const props = resolveConfig(input, DROP_PROPS_SCHEMA, 'Drop');
|
|
173
170
|
return {
|
|
174
171
|
name: props.name as string | null,
|
|
175
172
|
mode: props.mode as DropMode,
|
|
@@ -3,7 +3,7 @@ import { insert, jsx } from 'vanilla-signal';
|
|
|
3
3
|
import { type DOMReference, resolveElement } from '../utilities/dom.ts';
|
|
4
4
|
import { randomId } from '../utilities/id.ts';
|
|
5
5
|
import { timer } from '../utilities/timer.ts';
|
|
6
|
-
import { type
|
|
6
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
7
7
|
|
|
8
8
|
type ParabolaDirection =
|
|
9
9
|
| 'center'
|
|
@@ -81,10 +81,7 @@ const PARABOLA_PROPS_SCHEMA = {
|
|
|
81
81
|
ball: {
|
|
82
82
|
default: DEFAULT_BALL,
|
|
83
83
|
type: 'plainObject',
|
|
84
|
-
|
|
85
|
-
...DEFAULT_BALL,
|
|
86
|
-
...(value && typeof value === 'object' ? value : {}),
|
|
87
|
-
}),
|
|
84
|
+
merge: 'shallow',
|
|
88
85
|
shape: {
|
|
89
86
|
color: 'string',
|
|
90
87
|
size: 'string',
|
|
@@ -92,11 +89,8 @@ const PARABOLA_PROPS_SCHEMA = {
|
|
|
92
89
|
},
|
|
93
90
|
className: {
|
|
94
91
|
default: DEFAULT_CLASS_NAMES,
|
|
95
|
-
type: '
|
|
96
|
-
|
|
97
|
-
...DEFAULT_CLASS_NAMES,
|
|
98
|
-
...(value && typeof value === 'object' ? value : {}),
|
|
99
|
-
}),
|
|
92
|
+
type: 'plainObject',
|
|
93
|
+
merge: 'shallow',
|
|
100
94
|
},
|
|
101
95
|
from: { default: null, ...ELEMENT_REF_RULE },
|
|
102
96
|
to: { default: null, ...ELEMENT_REF_RULE },
|
|
@@ -108,10 +102,10 @@ const PARABOLA_PROPS_SCHEMA = {
|
|
|
108
102
|
showDelay: { default: 0, type: 'number' },
|
|
109
103
|
onShow: { default: null, types: ['function', 'null'] },
|
|
110
104
|
onHidden: { default: null, types: ['function', 'null'] },
|
|
111
|
-
} satisfies
|
|
105
|
+
} satisfies ConfigSchema<ParabolaProps>;
|
|
112
106
|
|
|
113
107
|
function normalizeProps(input: ParabolaProps): ResolvedParabolaProps {
|
|
114
|
-
const props =
|
|
108
|
+
const props = resolveConfig(input, PARABOLA_PROPS_SCHEMA, 'Parabola.props');
|
|
115
109
|
return {
|
|
116
110
|
ball: props.ball as ParabolaBallProps,
|
|
117
111
|
className: props.className as ParabolaClassNames,
|
package/src/primitives/popup.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ElementProps, jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
3
|
import { asRenderable, type RenderableContent } from '../utilities/dom.ts';
|
|
4
|
-
import { type
|
|
4
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
5
5
|
|
|
6
6
|
export interface PopupProps extends Record<string, unknown> {
|
|
7
7
|
className?: string;
|
|
@@ -28,10 +28,10 @@ const POPUP_PROPS_SCHEMA = {
|
|
|
28
28
|
default: '',
|
|
29
29
|
type: 'renderable',
|
|
30
30
|
},
|
|
31
|
-
} satisfies
|
|
31
|
+
} satisfies ConfigSchema<PopupProps>;
|
|
32
32
|
|
|
33
33
|
function normalizeProps(input: PopupProps): ResolvedPopupProps {
|
|
34
|
-
const props =
|
|
34
|
+
const props = resolveConfig(input, POPUP_PROPS_SCHEMA, 'Popup');
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
37
|
className: props.className as string,
|
package/src/primitives/sticky.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createDeepStore, flushSync } from 'vanilla-signal';
|
|
2
2
|
import { randomId } from '../utilities/id.ts';
|
|
3
3
|
import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
|
|
4
|
-
import { type
|
|
4
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
5
5
|
|
|
6
6
|
type StickyOverflow = 'destroy' | 'ignore';
|
|
7
7
|
|
|
@@ -91,10 +91,10 @@ const STICKY_PROPS_SCHEMA = {
|
|
|
91
91
|
},
|
|
92
92
|
reactive: { default: false, type: 'boolean' },
|
|
93
93
|
onReBuild: { default: null, types: ['function', 'null'] },
|
|
94
|
-
} satisfies
|
|
94
|
+
} satisfies ConfigSchema<StickyProps>;
|
|
95
95
|
|
|
96
96
|
function normalizeProps(input: StickyProps): ResolvedStickyProps {
|
|
97
|
-
const props =
|
|
97
|
+
const props = resolveConfig(input, STICKY_PROPS_SCHEMA, 'Sticky.props');
|
|
98
98
|
return {
|
|
99
99
|
target: props.target as DOMReference,
|
|
100
100
|
parent: props.parent as DOMReference,
|
package/src/primitives/theme.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { translate } from '../utilities/locale.ts';
|
|
|
11
11
|
import { all, joinClasses } from '../utilities/dom.ts';
|
|
12
12
|
import { createEventManager } from '../utilities/events.ts';
|
|
13
13
|
import { isPlainObject } from '../utilities/object.ts';
|
|
14
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
15
|
+
import { mergeShallowConfig } from '../utilities/merge.ts';
|
|
14
16
|
|
|
15
17
|
export type ThemeConfigKey = 'mode' | 'theme' | 'radius' | 'shadow' | 'font';
|
|
16
18
|
|
|
@@ -101,16 +103,26 @@ const THEME_CLASS_PREFIX: Record<Exclude<ThemeConfigKey, 'mode'>, string> = {
|
|
|
101
103
|
font: 'j-font-',
|
|
102
104
|
};
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
106
|
+
const THEME_OPTIONS_SCHEMA = {
|
|
107
|
+
mode: { default: DEFAULT_OPTIONS.mode, type: 'string' },
|
|
108
|
+
theme: { default: DEFAULT_OPTIONS.theme, type: 'string' },
|
|
109
|
+
radius: { default: DEFAULT_OPTIONS.radius, type: 'string' },
|
|
110
|
+
shadow: { default: DEFAULT_OPTIONS.shadow, type: 'string' },
|
|
111
|
+
font: { default: DEFAULT_OPTIONS.font, type: 'string' },
|
|
112
|
+
key: { default: DEFAULT_OPTIONS.key, type: 'string' },
|
|
113
|
+
className: {
|
|
114
|
+
default: DEFAULT_CLASS_NAMES,
|
|
115
|
+
type: 'plainObject',
|
|
116
|
+
merge: 'shallow',
|
|
117
|
+
},
|
|
118
|
+
} satisfies ConfigSchema<ThemeOptions>;
|
|
107
119
|
|
|
108
120
|
function normalizeOptions(options: ThemeOptions = {}): ThemeResolvedOptions {
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
return resolveConfig(
|
|
122
|
+
options,
|
|
123
|
+
THEME_OPTIONS_SCHEMA,
|
|
124
|
+
'Theme.options'
|
|
125
|
+
) as unknown as ThemeResolvedOptions;
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
function isThemeConfigKey(value: string | undefined): value is ThemeConfigKey {
|
|
@@ -340,14 +352,17 @@ export function createTheme(options: ThemeOptions = {}): ThemeInstance {
|
|
|
340
352
|
return panel;
|
|
341
353
|
};
|
|
342
354
|
const setConfig = (newConfig: ThemeOptions): void => {
|
|
355
|
+
const config = normalizeOptions({
|
|
356
|
+
...props,
|
|
357
|
+
...newConfig,
|
|
358
|
+
className: mergeShallowConfig(
|
|
359
|
+
props.className,
|
|
360
|
+
newConfig.className ?? {}
|
|
361
|
+
) as ThemeClassNames,
|
|
362
|
+
});
|
|
343
363
|
runtime.configVersion += 1;
|
|
344
364
|
flushSync(() => {
|
|
345
|
-
Object.assign(props,
|
|
346
|
-
className: mergeClassNames({
|
|
347
|
-
...props.className,
|
|
348
|
-
...newConfig.className,
|
|
349
|
-
}),
|
|
350
|
-
});
|
|
365
|
+
Object.assign(props, config);
|
|
351
366
|
});
|
|
352
367
|
saveConfig();
|
|
353
368
|
};
|
package/src/primitives/toast.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createLoading } from '../primitives/loading.ts';
|
|
|
12
12
|
import { joinClasses, q } from '../utilities/dom.ts';
|
|
13
13
|
import { listen } from '../utilities/events.ts';
|
|
14
14
|
import { randomId } from '../utilities/id.ts';
|
|
15
|
+
import { mergeShallowConfig } from '../utilities/merge.ts';
|
|
15
16
|
import { timer } from '../utilities/timer.ts';
|
|
16
17
|
import { validateParam } from '../utilities/types.ts';
|
|
17
18
|
|
|
@@ -98,14 +99,6 @@ const operations = new Map<HTMLElement, number>();
|
|
|
98
99
|
const singletonToasts = new Map<'show' | 'confirm', HTMLElement>();
|
|
99
100
|
let classNames = DEFAULT_CLASS_NAMES;
|
|
100
101
|
|
|
101
|
-
function mergeClassNames(value?: ToastClassNameConfig): ToastClassNames {
|
|
102
|
-
return { ...DEFAULT_CLASS_NAMES, ...value };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function resolveClassNames(options?: ToastClassNameOptions): ToastClassNames {
|
|
106
|
-
return mergeClassNames({ ...classNames, ...options?.className });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
102
|
function addToastDisposer(element: HTMLElement, dispose: () => void): void {
|
|
110
103
|
const current = disposers.get(element);
|
|
111
104
|
disposers.set(element, () => {
|
|
@@ -378,7 +371,10 @@ function show(message = '', options: ToastOptions = {}): HTMLElement {
|
|
|
378
371
|
validateParam('duration', duration, TOAST_DURATION_RULE, 'Toast.show');
|
|
379
372
|
validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.show');
|
|
380
373
|
|
|
381
|
-
const names =
|
|
374
|
+
const names = mergeShallowConfig(
|
|
375
|
+
classNames,
|
|
376
|
+
options.className ?? {}
|
|
377
|
+
) as ToastClassNames;
|
|
382
378
|
const id = randomId();
|
|
383
379
|
const iconElement = jsx('span', {
|
|
384
380
|
className: names.icon,
|
|
@@ -426,7 +422,10 @@ function lite(
|
|
|
426
422
|
validateParam('message', message, 'string', 'Toast.lite');
|
|
427
423
|
validateParam('duration', duration, LITE_DURATION_RULE, 'Toast.lite');
|
|
428
424
|
|
|
429
|
-
const names =
|
|
425
|
+
const names = mergeShallowConfig(
|
|
426
|
+
classNames,
|
|
427
|
+
className ?? {}
|
|
428
|
+
) as ToastClassNames;
|
|
430
429
|
const previous = q<HTMLElement>('[data-toast-lite]');
|
|
431
430
|
if (previous) {
|
|
432
431
|
cancelToastTimers(previous.dataset.toast || '');
|
|
@@ -457,7 +456,10 @@ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
|
|
|
457
456
|
const theme = props.theme || 'info';
|
|
458
457
|
validateParam('once', once, 'boolean', 'Toast.confirm');
|
|
459
458
|
validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.confirm');
|
|
460
|
-
const names =
|
|
459
|
+
const names = mergeShallowConfig(
|
|
460
|
+
classNames,
|
|
461
|
+
props.className ?? {}
|
|
462
|
+
) as ToastClassNames;
|
|
461
463
|
const id = randomId();
|
|
462
464
|
const element = jsx('div', {
|
|
463
465
|
className: joinClasses(names.confirm, names[theme]),
|
|
@@ -526,7 +528,10 @@ export const Toast = {
|
|
|
526
528
|
timers,
|
|
527
529
|
disposers,
|
|
528
530
|
configure(options: ToastClassNameOptions = {}): ToastClassNameOptions {
|
|
529
|
-
classNames =
|
|
531
|
+
classNames = mergeShallowConfig(
|
|
532
|
+
DEFAULT_CLASS_NAMES,
|
|
533
|
+
options.className ?? {}
|
|
534
|
+
) as ToastClassNames;
|
|
530
535
|
return { className: classNames };
|
|
531
536
|
},
|
|
532
537
|
show,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
3
|
import { type DOMReference, joinClasses } from '../utilities/dom.ts';
|
|
4
|
-
import { type
|
|
4
|
+
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
5
5
|
import { createDrop } from './drop.ts';
|
|
6
6
|
|
|
7
7
|
type DropInstance = ReturnType<typeof createDrop>;
|
|
@@ -102,20 +102,6 @@ const DEFAULT_CLASS_NAMES: TooltipClassNames = {
|
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
function normalizeClassNames(value: unknown): TooltipClassNames {
|
|
106
|
-
const input = value && typeof value === 'object' ? value : {};
|
|
107
|
-
const ui =
|
|
108
|
-
'ui' in input && input.ui && typeof input.ui === 'object' ? input.ui : {};
|
|
109
|
-
return {
|
|
110
|
-
...DEFAULT_CLASS_NAMES,
|
|
111
|
-
...input,
|
|
112
|
-
ui: {
|
|
113
|
-
...DEFAULT_CLASS_NAMES.ui,
|
|
114
|
-
...ui,
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
105
|
const TOOLTIP_OPTIONS_SCHEMA = {
|
|
120
106
|
name: { default: null, types: ['string', 'null'] },
|
|
121
107
|
mode: { default: 'hover', type: 'string', enum: ['hover', 'click'] },
|
|
@@ -155,18 +141,18 @@ const TOOLTIP_OPTIONS_SCHEMA = {
|
|
|
155
141
|
},
|
|
156
142
|
className: {
|
|
157
143
|
default: DEFAULT_CLASS_NAMES,
|
|
158
|
-
type: '
|
|
159
|
-
|
|
144
|
+
type: 'plainObject',
|
|
145
|
+
merge: 'deep',
|
|
160
146
|
},
|
|
161
147
|
id: { default: null, types: ['string', 'null'] },
|
|
162
148
|
delay: { default: 100, types: ['number', 'object'] },
|
|
163
149
|
hoverIntent: { default: true, type: 'boolean' },
|
|
164
150
|
onShown: { default: null, types: ['function', 'null'] },
|
|
165
151
|
onHidden: { default: null, types: ['function', 'null'] },
|
|
166
|
-
} satisfies
|
|
152
|
+
} satisfies ConfigSchema<TooltipProps>;
|
|
167
153
|
|
|
168
154
|
function normalizeProps(input: TooltipProps): ResolvedTooltipProps {
|
|
169
|
-
const props =
|
|
155
|
+
const props = resolveConfig(input, TOOLTIP_OPTIONS_SCHEMA, 'Tooltip.props');
|
|
170
156
|
return {
|
|
171
157
|
name: props.name as string | null,
|
|
172
158
|
mode: props.mode as DropMode,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type ParamRule, type ParamRuleInput, validateParam } from './types.ts';
|
|
2
|
+
import {
|
|
3
|
+
type MergeStrategy,
|
|
4
|
+
cloneConfigValue,
|
|
5
|
+
mergeConfigValue,
|
|
6
|
+
} from './merge.ts';
|
|
7
|
+
import { isPlainObject } from './object.ts';
|
|
8
|
+
|
|
9
|
+
type LooseRecord = Record<string, unknown>;
|
|
10
|
+
type ConfigTypeRule = string | readonly string[];
|
|
11
|
+
|
|
12
|
+
export interface ConfigNormalizeContext<
|
|
13
|
+
TInput extends LooseRecord = LooseRecord,
|
|
14
|
+
> {
|
|
15
|
+
key: string;
|
|
16
|
+
path: string;
|
|
17
|
+
input: TInput;
|
|
18
|
+
options: LooseRecord;
|
|
19
|
+
schema: ConfigSchema<TInput>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConfigRule<
|
|
23
|
+
TInput extends LooseRecord = LooseRecord,
|
|
24
|
+
> extends Omit<ParamRule<TInput>, 'normalize' | 'shape'> {
|
|
25
|
+
defaultFactory?: () => unknown;
|
|
26
|
+
merge?: MergeStrategy;
|
|
27
|
+
schema?: ConfigSchema;
|
|
28
|
+
normalize?: (
|
|
29
|
+
value: unknown,
|
|
30
|
+
context: ConfigNormalizeContext<TInput>
|
|
31
|
+
) => unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ConfigRuleInput<TInput extends LooseRecord = LooseRecord> =
|
|
35
|
+
| ConfigTypeRule
|
|
36
|
+
| ConfigRule<TInput>;
|
|
37
|
+
|
|
38
|
+
export type ConfigSchema<TInput extends LooseRecord = LooseRecord> = Record<
|
|
39
|
+
string,
|
|
40
|
+
ConfigRuleInput<TInput>
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
export type ResolvedConfig<TSchema extends object> = LooseRecord & {
|
|
44
|
+
[Key in keyof TSchema]: unknown;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function normalizeConfigRule<TInput extends LooseRecord>(
|
|
48
|
+
rule: ConfigRuleInput<TInput> = {}
|
|
49
|
+
): ConfigRule<TInput> {
|
|
50
|
+
if (typeof rule === 'string' || Array.isArray(rule)) return { type: rule };
|
|
51
|
+
if (!rule || typeof rule !== 'object') return {};
|
|
52
|
+
return rule as ConfigRule<TInput>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveConfigDefault<TInput extends LooseRecord>(
|
|
56
|
+
rule: ConfigRule<TInput>
|
|
57
|
+
): unknown {
|
|
58
|
+
if (typeof rule.defaultFactory === 'function') {
|
|
59
|
+
return cloneConfigValue(rule.defaultFactory());
|
|
60
|
+
}
|
|
61
|
+
if (Object.hasOwn(rule, 'default')) {
|
|
62
|
+
return cloneConfigValue(rule.default);
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isInputMissing(source: LooseRecord, key: string): boolean {
|
|
68
|
+
return !Object.hasOwn(source, key) || source[key] === undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveConfigValue<TInput extends LooseRecord>(
|
|
72
|
+
key: string,
|
|
73
|
+
rule: ConfigRule<TInput>,
|
|
74
|
+
source: TInput,
|
|
75
|
+
namespace: string
|
|
76
|
+
): unknown {
|
|
77
|
+
const path = namespace ? `${namespace}.${key}` : key;
|
|
78
|
+
const defaultValue = resolveConfigDefault(rule);
|
|
79
|
+
const value = isInputMissing(source, key)
|
|
80
|
+
? defaultValue
|
|
81
|
+
: mergeConfigValue(defaultValue, source[key], rule.merge, { key, path });
|
|
82
|
+
|
|
83
|
+
return rule.schema && (value === undefined || isPlainObject(value))
|
|
84
|
+
? resolveConfig(value as LooseRecord | undefined, rule.schema, path)
|
|
85
|
+
: value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateConfigValue<TInput extends LooseRecord>(
|
|
89
|
+
key: string,
|
|
90
|
+
value: unknown,
|
|
91
|
+
rule: ConfigRule<TInput>,
|
|
92
|
+
namespace: string
|
|
93
|
+
): void {
|
|
94
|
+
const validationRule: ParamRuleInput<TInput> = rule as ParamRule<TInput>;
|
|
95
|
+
validateParam(key, value, validationRule, namespace);
|
|
96
|
+
|
|
97
|
+
if (!rule.schema || !isPlainObject(value)) return;
|
|
98
|
+
|
|
99
|
+
const childNamespace = namespace ? `${namespace}.${key}` : key;
|
|
100
|
+
const valueRecord = value as LooseRecord;
|
|
101
|
+
for (const [childKey, childRule] of Object.entries(rule.schema)) {
|
|
102
|
+
validateConfigValue(
|
|
103
|
+
childKey,
|
|
104
|
+
valueRecord[childKey],
|
|
105
|
+
normalizeConfigRule(childRule),
|
|
106
|
+
childNamespace
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveConfig<
|
|
112
|
+
TInput extends LooseRecord,
|
|
113
|
+
TSchema extends ConfigSchema<TInput>,
|
|
114
|
+
>(
|
|
115
|
+
input: TInput | null | undefined = {} as TInput,
|
|
116
|
+
schema: TSchema = {} as TSchema,
|
|
117
|
+
namespace = 'Config'
|
|
118
|
+
): ResolvedConfig<TSchema> {
|
|
119
|
+
const source = input == null ? {} : input;
|
|
120
|
+
if (typeof source !== 'object' || Array.isArray(source)) {
|
|
121
|
+
throw new Error(`${namespace} expects object.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const sourceRecord = source as TInput;
|
|
125
|
+
const resolved: LooseRecord = {};
|
|
126
|
+
const entries = Object.entries(schema || {}) as Array<
|
|
127
|
+
[string, ConfigRuleInput<TInput>]
|
|
128
|
+
>;
|
|
129
|
+
|
|
130
|
+
for (const [key, rawRule] of entries) {
|
|
131
|
+
const rule = normalizeConfigRule<TInput>(rawRule);
|
|
132
|
+
resolved[key] = resolveConfigValue(key, rule, sourceRecord, namespace);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const [key, rawRule] of entries) {
|
|
136
|
+
const rule = normalizeConfigRule<TInput>(rawRule);
|
|
137
|
+
if (typeof rule.normalize === 'function') {
|
|
138
|
+
resolved[key] = rule.normalize(resolved[key], {
|
|
139
|
+
key,
|
|
140
|
+
path: namespace ? `${namespace}.${key}` : key,
|
|
141
|
+
input: sourceRecord,
|
|
142
|
+
options: resolved,
|
|
143
|
+
schema,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [key, rule] of entries) {
|
|
149
|
+
validateConfigValue(
|
|
150
|
+
key,
|
|
151
|
+
resolved[key],
|
|
152
|
+
normalizeConfigRule<TInput>(rule),
|
|
153
|
+
namespace
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return resolved as ResolvedConfig<TSchema>;
|
|
158
|
+
}
|
package/src/utilities/id.ts
CHANGED
|
@@ -73,6 +73,125 @@ type QueryParamValue =
|
|
|
73
73
|
*/
|
|
74
74
|
type QueryParams = Record<string, QueryParamValue>;
|
|
75
75
|
|
|
76
|
+
// const SHA256_K = new Uint32Array([
|
|
77
|
+
// 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
78
|
+
// 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
79
|
+
// 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
80
|
+
// 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
81
|
+
// 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
82
|
+
// 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
83
|
+
// 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
84
|
+
// 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
85
|
+
// 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
86
|
+
// 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
87
|
+
// 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
88
|
+
// ]);
|
|
89
|
+
|
|
90
|
+
// const SHA256_INITIAL = new Uint32Array([
|
|
91
|
+
// 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
92
|
+
// 0x1f83d9ab, 0x5be0cd19,
|
|
93
|
+
// ]);
|
|
94
|
+
|
|
95
|
+
// function rotateRight(value: number, bits: number): number {
|
|
96
|
+
// return (value >>> bits) | (value << (32 - bits));
|
|
97
|
+
// }
|
|
98
|
+
|
|
99
|
+
// function sha256Fallback(input: Uint8Array): Uint8Array {
|
|
100
|
+
// const message = new Uint8Array(Math.ceil((input.length + 9) / 64) * 64);
|
|
101
|
+
// message.set(input);
|
|
102
|
+
// message[input.length] = 0x80;
|
|
103
|
+
|
|
104
|
+
// const bitLength = BigInt(input.length) * 8n;
|
|
105
|
+
// for (let i = 0; i < 8; i += 1) {
|
|
106
|
+
// message[message.length - 1 - i] = Number(
|
|
107
|
+
// (bitLength >> BigInt(i * 8)) & 0xffn
|
|
108
|
+
// );
|
|
109
|
+
// }
|
|
110
|
+
|
|
111
|
+
// const hash = new Uint32Array(SHA256_INITIAL);
|
|
112
|
+
// const words = new Uint32Array(64);
|
|
113
|
+
|
|
114
|
+
// for (let offset = 0; offset < message.length; offset += 64) {
|
|
115
|
+
// for (let i = 0; i < 16; i += 1) {
|
|
116
|
+
// const index = offset + i * 4;
|
|
117
|
+
// words[i] =
|
|
118
|
+
// ((message[index] << 24) |
|
|
119
|
+
// (message[index + 1] << 16) |
|
|
120
|
+
// (message[index + 2] << 8) |
|
|
121
|
+
// message[index + 3]) >>>
|
|
122
|
+
// 0;
|
|
123
|
+
// }
|
|
124
|
+
|
|
125
|
+
// for (let i = 16; i < 64; i += 1) {
|
|
126
|
+
// const s0 =
|
|
127
|
+
// rotateRight(words[i - 15], 7) ^
|
|
128
|
+
// rotateRight(words[i - 15], 18) ^
|
|
129
|
+
// (words[i - 15] >>> 3);
|
|
130
|
+
// const s1 =
|
|
131
|
+
// rotateRight(words[i - 2], 17) ^
|
|
132
|
+
// rotateRight(words[i - 2], 19) ^
|
|
133
|
+
// (words[i - 2] >>> 10);
|
|
134
|
+
// words[i] = (words[i - 16] + s0 + words[i - 7] + s1) >>> 0;
|
|
135
|
+
// }
|
|
136
|
+
|
|
137
|
+
// let a = hash[0];
|
|
138
|
+
// let b = hash[1];
|
|
139
|
+
// let c = hash[2];
|
|
140
|
+
// let d = hash[3];
|
|
141
|
+
// let e = hash[4];
|
|
142
|
+
// let f = hash[5];
|
|
143
|
+
// let g = hash[6];
|
|
144
|
+
// let h = hash[7];
|
|
145
|
+
|
|
146
|
+
// for (let i = 0; i < 64; i += 1) {
|
|
147
|
+
// const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
148
|
+
// const ch = (e & f) ^ (~e & g);
|
|
149
|
+
// const temp1 = (h + s1 + ch + SHA256_K[i] + words[i]) >>> 0;
|
|
150
|
+
// const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
151
|
+
// const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
152
|
+
// const temp2 = (s0 + maj) >>> 0;
|
|
153
|
+
|
|
154
|
+
// h = g;
|
|
155
|
+
// g = f;
|
|
156
|
+
// f = e;
|
|
157
|
+
// e = (d + temp1) >>> 0;
|
|
158
|
+
// d = c;
|
|
159
|
+
// c = b;
|
|
160
|
+
// b = a;
|
|
161
|
+
// a = (temp1 + temp2) >>> 0;
|
|
162
|
+
// }
|
|
163
|
+
|
|
164
|
+
// hash[0] = (hash[0] + a) >>> 0;
|
|
165
|
+
// hash[1] = (hash[1] + b) >>> 0;
|
|
166
|
+
// hash[2] = (hash[2] + c) >>> 0;
|
|
167
|
+
// hash[3] = (hash[3] + d) >>> 0;
|
|
168
|
+
// hash[4] = (hash[4] + e) >>> 0;
|
|
169
|
+
// hash[5] = (hash[5] + f) >>> 0;
|
|
170
|
+
// hash[6] = (hash[6] + g) >>> 0;
|
|
171
|
+
// hash[7] = (hash[7] + h) >>> 0;
|
|
172
|
+
// }
|
|
173
|
+
|
|
174
|
+
// const output = new Uint8Array(32);
|
|
175
|
+
// for (let i = 0; i < hash.length; i += 1) {
|
|
176
|
+
// output[i * 4] = hash[i] >>> 24;
|
|
177
|
+
// output[i * 4 + 1] = hash[i] >>> 16;
|
|
178
|
+
// output[i * 4 + 2] = hash[i] >>> 8;
|
|
179
|
+
// output[i * 4 + 3] = hash[i];
|
|
180
|
+
// }
|
|
181
|
+
// return output;
|
|
182
|
+
// }
|
|
183
|
+
|
|
184
|
+
async function sha256(input: Uint8Array): Promise<Uint8Array> {
|
|
185
|
+
const subtle = globalThis.crypto?.subtle;
|
|
186
|
+
if (subtle && typeof subtle.digest === 'function') {
|
|
187
|
+
const source = new ArrayBuffer(input.byteLength);
|
|
188
|
+
new Uint8Array(source).set(input);
|
|
189
|
+
return new Uint8Array(await subtle.digest('SHA-256', source));
|
|
190
|
+
}
|
|
191
|
+
// return sha256Fallback(input);
|
|
192
|
+
throw new Error('hashQueryParams only works in secure context.');
|
|
193
|
+
}
|
|
194
|
+
|
|
76
195
|
/**
|
|
77
196
|
* 对查询参数进行 SHA-256 哈希编码,生成用于缓存的 key
|
|
78
197
|
*
|
|
@@ -114,11 +233,11 @@ export async function hashQueryParams(
|
|
|
114
233
|
const buffer = new TextEncoder().encode(str);
|
|
115
234
|
|
|
116
235
|
// 4. SHA-256 哈希
|
|
117
|
-
const hashBuffer = await
|
|
236
|
+
const hashBuffer = await sha256(buffer);
|
|
118
237
|
|
|
119
238
|
// 5. 取前 N 字节(防越界)
|
|
120
239
|
const safeBytes = Math.min(bytes, hashBuffer.byteLength);
|
|
121
|
-
const slice =
|
|
240
|
+
const slice = hashBuffer.subarray(0, safeBytes);
|
|
122
241
|
|
|
123
242
|
// 6. 转为 BigInt 再转 36 进制
|
|
124
243
|
let num = 0n;
|