@saolabs/client 1.3.0 → 1.5.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/src/core/app/Application.d.ts +2 -0
- package/dist/src/core/app/Application.d.ts.map +1 -1
- package/dist/src/core/app/Application.js +7 -0
- package/dist/src/core/app/Application.js.map +1 -1
- package/dist/src/core/bootstrap/app.d.ts.map +1 -1
- package/dist/src/core/bootstrap/app.js +8 -2
- package/dist/src/core/bootstrap/app.js.map +1 -1
- package/dist/src/core/bootstrap/bundle.d.ts +7 -3
- package/dist/src/core/bootstrap/bundle.d.ts.map +1 -1
- package/dist/src/core/bootstrap/bundle.js +84 -7
- package/dist/src/core/bootstrap/bundle.js.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.js +11 -3
- package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
- package/dist/src/core/contracts/BundleInterface.d.ts +29 -0
- package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -1
- package/dist/src/core/elements/Html.d.ts +1 -0
- package/dist/src/core/elements/Html.d.ts.map +1 -1
- package/dist/src/core/elements/Html.js +18 -0
- package/dist/src/core/elements/Html.js.map +1 -1
- package/dist/src/core/services/HelperService.d.ts +2 -0
- package/dist/src/core/services/HelperService.d.ts.map +1 -1
- package/dist/src/core/services/HelperService.js +13 -1
- package/dist/src/core/services/HelperService.js.map +1 -1
- package/dist/src/core/services/TranslationService.d.ts +2 -0
- package/dist/src/core/services/TranslationService.d.ts.map +1 -1
- package/dist/src/core/services/TranslationService.js +5 -0
- package/dist/src/core/services/TranslationService.js.map +1 -1
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/core/app/Application.ts +6 -0
- package/src/core/bootstrap/app.ts +9 -3
- package/src/core/bootstrap/bundle.ts +83 -7
- package/src/core/bootstrap/providers/default-providers.ts +9 -3
- package/src/core/contracts/BundleInterface.ts +31 -0
- package/src/core/elements/Html.ts +18 -0
- package/src/core/services/HelperService.ts +13 -1
- package/src/core/services/TranslationService.ts +6 -0
|
@@ -26,11 +26,12 @@ const SYSTEM_SERVICE_KEYS = new Set(['Store', 'Storage', 'Event', 'Http', 'View'
|
|
|
26
26
|
* services: { Auth: AuthService, Toast: ToastService }
|
|
27
27
|
* → 2 providers, mỗi cái dependsOn ['core'], register = app.set(name, new Class(app))
|
|
28
28
|
*/
|
|
29
|
-
function servicesFromMap(map: Record<string, any
|
|
29
|
+
function servicesFromMap(map: Record<string, any>, strict = false): ServiceProviderInterface[] {
|
|
30
30
|
const appInstance = app<ApplicationInterface>();
|
|
31
31
|
return Object.entries(map)
|
|
32
32
|
.filter(([name]) => {
|
|
33
33
|
if (SYSTEM_SERVICE_KEYS.has(name)) {
|
|
34
|
+
if (strict) throw new Error(`[Bootstrap] Không được ghi đè service hệ thống "${name}" qua config.services.`);
|
|
34
35
|
console.warn(`[Bootstrap] Cannot override system service "${name}" via config.services. Ignored.`);
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
@@ -44,7 +45,9 @@ function servicesFromMap(map: Record<string, any>): ServiceProviderInterface[] {
|
|
|
44
45
|
// không phải class — `new` vô điều kiện sẽ ném "is not a
|
|
45
46
|
// constructor". Chỉ dựng khi thật sự là class.
|
|
46
47
|
const isClass = typeof value === 'function' && value.prototype;
|
|
47
|
-
|
|
48
|
+
// Dạng `{ factory }`: mỗi Application dựng instance riêng.
|
|
49
|
+
const isFactory = !!value && typeof value === 'object' && typeof value.factory === 'function';
|
|
50
|
+
appInstance.set(name, isClass ? new value(appInstance) : isFactory ? value.factory(appInstance) : value);
|
|
48
51
|
}
|
|
49
52
|
}));
|
|
50
53
|
}
|
|
@@ -92,6 +95,8 @@ export function buildDefaultProviders(config: Record<string, any> = {}): NamedSe
|
|
|
92
95
|
HelperServiceProvider.config = { i18n: config.i18n };
|
|
93
96
|
ApiServiceProvider.config = config.api || {};
|
|
94
97
|
const App = app<ApplicationInterface>();
|
|
98
|
+
const strict = !!config.strictBundles;
|
|
99
|
+
App.strictBundles = strict;
|
|
95
100
|
const defaults: NamedServiceProvider[] = [
|
|
96
101
|
CoreServiceProvider,
|
|
97
102
|
ViewServiceProvider,
|
|
@@ -102,7 +107,7 @@ export function buildDefaultProviders(config: Record<string, any> = {}): NamedSe
|
|
|
102
107
|
|
|
103
108
|
// config.services: { Name: Class } → auto-wrap providers
|
|
104
109
|
const serviceProviders = config.services
|
|
105
|
-
? servicesFromMap(config.services)
|
|
110
|
+
? servicesFromMap(config.services, strict)
|
|
106
111
|
: [];
|
|
107
112
|
|
|
108
113
|
// config.helpers: { tên: fn } → một provider phụ thuộc `helper`
|
|
@@ -118,6 +123,7 @@ export function buildDefaultProviders(config: Record<string, any> = {}): NamedSe
|
|
|
118
123
|
)
|
|
119
124
|
.filter(p => {
|
|
120
125
|
if (SYSTEM_PROVIDER_NAMES.has(p.name as any)) {
|
|
126
|
+
if (strict) throw new Error(`[Bootstrap] Không được ghi đè provider hệ thống "${p.name}".`);
|
|
121
127
|
console.warn(`[Bootstrap] Cannot override system provider "${p.name}". Ignored.`);
|
|
122
128
|
return false;
|
|
123
129
|
}
|
|
@@ -22,10 +22,41 @@ export interface SaolaBundle {
|
|
|
22
22
|
helpers?: Record<string, any>;
|
|
23
23
|
/** { 'dot.path': factory } → View.setViewRegistry(). Nạp sau ĐÈ nạp trước. */
|
|
24
24
|
views?: Record<string, ViewFactory>;
|
|
25
|
+
/**
|
|
26
|
+
* Khoá (service/helper/view) bundle này CỐ Ý thay của bundle trước. Chỉ có hiệu
|
|
27
|
+
* lực ở chế độ strict và chỉ khi khoá nằm trong `overridable` của context.
|
|
28
|
+
*/
|
|
29
|
+
overrides?: string[];
|
|
30
|
+
/** `'theme'` = lớp trình bày, bị giới hạn khoá (xem MergeOptions.themeNamespaces). */
|
|
31
|
+
kind?: 'theme' | 'package';
|
|
25
32
|
/** Chạy sau khi mọi bundle đã merge và App đã init. */
|
|
26
33
|
boot?(app: ApplicationInterface): void;
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Dạng khai service có factory: App gọi `factory(app)` khi đăng ký, nên mỗi
|
|
38
|
+
* Application có instance RIÊNG (không dùng instance tạo ở phạm vi module).
|
|
39
|
+
*/
|
|
40
|
+
export interface ServiceFactory<T = any> {
|
|
41
|
+
factory: (app: ApplicationInterface) => T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tuỳ chọn gộp bundle. Mặc định (không truyền) = hành vi cũ: sau đè trước, im lặng. */
|
|
45
|
+
export interface MergeOptions {
|
|
46
|
+
/** Bật kiểm: trùng khoá, provider trùng tên, giới hạn khoá của theme. */
|
|
47
|
+
strict?: boolean;
|
|
48
|
+
/** Khoá được phép `overrides`; mục `A.B` phủ cả `A.B.x`. */
|
|
49
|
+
overridable?: string[];
|
|
50
|
+
/** Với bundle `kind: 'theme'`: tiền tố khoá services/helpers được phép. Thiếu = không cho khoá nào. */
|
|
51
|
+
themeNamespaces?: string[];
|
|
52
|
+
/** Ép mọi bundle trong lượt gộp là loại này (server quyết, bundle không tự nâng quyền được). */
|
|
53
|
+
kind?: 'theme' | 'package';
|
|
54
|
+
/** Trusted server classification by URL; bundle contents cannot promote a theme. */
|
|
55
|
+
bundleKinds?: Record<string, 'theme' | 'package'>;
|
|
56
|
+
/** Already registered source services/providers participate in strict collision checks. */
|
|
57
|
+
initialBundle?: SaolaBundle;
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
/** Kết quả gộp nhiều bundle, theo đúng thứ tự mảng URL. */
|
|
30
61
|
export interface MergedBundles {
|
|
31
62
|
providers: (ServiceProviderInterface | any)[];
|
|
@@ -732,6 +732,13 @@ export class Html implements HtmlInterface {
|
|
|
732
732
|
if (this.isSingleElement()) {
|
|
733
733
|
return this.element;
|
|
734
734
|
}
|
|
735
|
+
// Editors own their inner DOM, selection and composition state. Reconcile the
|
|
736
|
+
// host's attributes/events normally, but leave an explicitly declared island alone.
|
|
737
|
+
if (this.element.hasAttribute('data-sao-preserve-children')) {
|
|
738
|
+
this.initMode = InitModes.CREATE;
|
|
739
|
+
this.maybeRunEnter();
|
|
740
|
+
return this.element;
|
|
741
|
+
}
|
|
735
742
|
if (this.config.content) {
|
|
736
743
|
this.renderTextContent();
|
|
737
744
|
return this.element;
|
|
@@ -750,6 +757,7 @@ export class Html implements HtmlInterface {
|
|
|
750
757
|
if (children && children.length > 0) {
|
|
751
758
|
hydrateElementList(this, children);
|
|
752
759
|
}
|
|
760
|
+
this.applyOptionSelection();
|
|
753
761
|
this.initMode = InitModes.CREATE;
|
|
754
762
|
return this.element;
|
|
755
763
|
}
|
|
@@ -768,11 +776,21 @@ export class Html implements HtmlInterface {
|
|
|
768
776
|
}
|
|
769
777
|
}
|
|
770
778
|
for (const node of Array.from(this.element.childNodes)) if (!keep.has(node)) node.remove();
|
|
779
|
+
this.applyOptionSelection();
|
|
771
780
|
if (this.isStarted) for (const child of children as any[]) child.start?.();
|
|
772
781
|
this.maybeRunEnter();
|
|
773
782
|
return this.element;
|
|
774
783
|
}
|
|
775
784
|
|
|
785
|
+
private applyOptionSelection(): void {
|
|
786
|
+
const selected = this.config.props?.selected;
|
|
787
|
+
if (this.tagName.toLowerCase() !== 'option' || !selected) return;
|
|
788
|
+
// Insertion into a select may choose an option automatically. Apply the
|
|
789
|
+
// declared selection again after its DOM and text children are mounted.
|
|
790
|
+
(this.element as HTMLOptionElement).selected = Boolean(selected.type === 'binding'
|
|
791
|
+
? selected.factory?.() : selected.value);
|
|
792
|
+
}
|
|
793
|
+
|
|
776
794
|
private renderTextContent(): void {
|
|
777
795
|
this.contentUnsubscribe?.();
|
|
778
796
|
this.contentUnsubscribe = null;
|
|
@@ -78,13 +78,16 @@ export class HelperService implements HelperInterface {
|
|
|
78
78
|
}
|
|
79
79
|
// Page-data refreshes revisions without resetting the chosen locale.
|
|
80
80
|
this.translations.refresh(config);
|
|
81
|
+
this.syncHeld();
|
|
81
82
|
}
|
|
82
83
|
text(key: string, replace: Record<string, any> | string | null = {}, defaultValue: string | null = null, locale?: string): string {
|
|
83
84
|
if (typeof replace === 'string' && defaultValue === null) defaultValue = replace;
|
|
84
85
|
return this.translations.text(key, replace && typeof replace === 'object' ? replace : {}, defaultValue, locale);
|
|
85
86
|
}
|
|
86
87
|
installLanguage(locale: string, snapshot: import('./TranslationService.js').LanguageSnapshot): boolean {
|
|
87
|
-
|
|
88
|
+
const installed = this.translations.install(locale, snapshot);
|
|
89
|
+
this.syncHeld();
|
|
90
|
+
return installed;
|
|
88
91
|
}
|
|
89
92
|
async loadLanguage(locale: string, load: (locale: string) => Promise<import('./TranslationService.js').LanguageSnapshot>): Promise<boolean> {
|
|
90
93
|
return this.installLanguage(locale, await load(locale));
|
|
@@ -123,6 +126,15 @@ export class HelperService implements HelperInterface {
|
|
|
123
126
|
if (typeof document !== 'undefined') document.documentElement.lang = locale;
|
|
124
127
|
this.App?.Http?.setHeader?.('Accept-Language', locale);
|
|
125
128
|
this.App?.API?.getHttpService?.()?.setHeader?.('Accept-Language', locale);
|
|
129
|
+
this.syncHeld();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Tells the server which dictionaries this app already holds, so page data does not carry them again. */
|
|
133
|
+
private syncHeld(): void {
|
|
134
|
+
const held = this.translations.held();
|
|
135
|
+
if (!held) return;
|
|
136
|
+
this.App?.Http?.setHeader?.('X-Saola-Language', held);
|
|
137
|
+
this.App?.API?.getHttpService?.()?.setHeader?.('X-Saola-Language', held);
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
// ─── Execution ──────────────────────────────────────────────
|
|
@@ -53,6 +53,12 @@ export class TranslationService {
|
|
|
53
53
|
return this.interpolate(value, replace);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** The catalogs this app holds, as the server reads them back: `<scope>;<locale>=<revision>;…` ('' while none is loaded). */
|
|
57
|
+
held(): string {
|
|
58
|
+
const parts = Object.entries(this.catalogs).map(([locale, snapshot]) => `${locale}=${snapshot.revision}`);
|
|
59
|
+
return this.catalogScope && parts.length ? [this.catalogScope, ...parts].join(';') : '';
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
install(locale: string, snapshot: LanguageSnapshot): boolean {
|
|
57
63
|
if (snapshot.scope !== this.catalogScope) throw new Error('Language snapshot scope differs from the active catalog');
|
|
58
64
|
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0) throw new Error('Invalid language revision');
|