@nutin/cli 1.2.3 → 1.3.1
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/CHANGELOG.md +70 -1
- package/README.md +25 -18
- package/lib/index.mjs +23 -20
- package/lib/src/context-builder.mjs +40 -0
- package/lib/src/file-generator.mjs +26 -126
- package/lib/src/git-manager.mjs +1 -1
- package/lib/src/json-generator.mjs +31 -0
- package/lib/src/package-manager.mjs +21 -18
- package/lib/src/print.mjs +1 -0
- package/lib/src/project-generator.mjs +53 -0
- package/lib/src/prompts.mjs +16 -75
- package/lib/src/template-compiler.mjs +1 -1
- package/lib/src/utils.mjs +9 -9
- package/lib/src/version.mjs +1 -0
- package/package.json +1 -1
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/builder.config.js.hbs +8 -0
- package/templates/base/src/app/globals.d.ts.hbs +12 -8
- package/templates/base/src/app/main.ts.hbs +2 -1
- package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +46 -26
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +36 -22
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +60 -13
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +16 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
- package/templates/base/src/core/base-classes/component/component.ts.hbs +20 -24
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +19 -2
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
- package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +15 -14
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +4 -36
- package/templates/base/src/core/services/router/router.ts.hbs +45 -46
- package/templates/base/src/core/services/store/store.ts.hbs +6 -6
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +41 -15
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +40 -19
- package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
- package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
- package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
- package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
- package/templates/base/src/libs/popover/popover.ts.hbs +89 -96
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +7 -4
- package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
- package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
- package/templates/base/src/styles/core/_index.scss.hbs +6 -0
- package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
- package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/base/tools/builder/builder.js.hbs +9 -3
- package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
- package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
- package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
- package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
- package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
- package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
- package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
- package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
- package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +76 -52
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +31 -0
- package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
- package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
- package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
- package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
- /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
|
@@ -3,9 +3,9 @@ import { TokenHelper } from './token.helper.js';
|
|
|
3
3
|
|
|
4
4
|
export class EventHelper {
|
|
5
5
|
public static bindEvents(
|
|
6
|
-
component: BaseComponent,
|
|
7
|
-
element: HTMLElement,
|
|
8
|
-
eventListeners: Array<[string, EventListener]>
|
|
6
|
+
component: BaseComponent,
|
|
7
|
+
element: HTMLElement,
|
|
8
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
9
9
|
): void {
|
|
10
10
|
element.querySelectorAll('[data-event]').forEach(el => {
|
|
11
11
|
const parts = el.getAttribute('data-event')!.split(':');
|
|
@@ -23,6 +23,14 @@ export class EventHelper {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
public static destroyEvents(
|
|
27
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
28
|
+
): void {
|
|
29
|
+
eventListeners.forEach(([target, event, listener]) => {
|
|
30
|
+
target.removeEventListener(event, listener);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
private static createBoundHandler(
|
|
27
35
|
el: Element,
|
|
28
36
|
component: BaseComponent,
|
|
@@ -37,21 +45,12 @@ export class EventHelper {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
private static addEvent(
|
|
40
|
-
target: EventTarget,
|
|
41
|
-
event: string,
|
|
42
|
-
listener: EventListener,
|
|
43
|
-
eventListeners: Array<[string, EventListener]>
|
|
48
|
+
target: EventTarget,
|
|
49
|
+
event: string,
|
|
50
|
+
listener: EventListener,
|
|
51
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
44
52
|
): void {
|
|
45
53
|
target.addEventListener(event, listener);
|
|
46
|
-
eventListeners.push([event, listener]);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public static destroyEvents(
|
|
50
|
-
element: HTMLElement,
|
|
51
|
-
eventListeners: Array<[string, EventListener]>
|
|
52
|
-
): void {
|
|
53
|
-
eventListeners.forEach(([event, listener]) => {
|
|
54
|
-
element.removeEventListener(event, listener);
|
|
55
|
-
});
|
|
54
|
+
eventListeners.push([target, event, listener]);
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
export type TrustLevel = 'strict' | 'normal' | 'trusted';
|
|
2
|
+
|
|
1
3
|
export class SecurityHelper {
|
|
2
|
-
public static
|
|
4
|
+
public static sanitizeTemplate(value: unknown, trustLevel: TrustLevel = 'normal'): string {
|
|
3
5
|
if (value === null || value === undefined) return '';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.replace(/</g, "<")
|
|
8
|
-
.replace(/>/g, ">")
|
|
9
|
-
.replace(/"/g, """)
|
|
10
|
-
.replace(/'/g, "'");
|
|
6
|
+
let template = String(value);
|
|
7
|
+
|
|
8
|
+
return this.sanitizeInputTemplate(template, trustLevel);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
public static sanitizeInputElement(
|
|
@@ -21,4 +19,38 @@ export class SecurityHelper {
|
|
|
21
19
|
}
|
|
22
20
|
return '';
|
|
23
21
|
}
|
|
22
|
+
|
|
23
|
+
public static escapeHtml(value: unknown): string {
|
|
24
|
+
if (value === null || value === undefined) return '';
|
|
25
|
+
const str = String(value);
|
|
26
|
+
return str
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """)
|
|
31
|
+
.replace(/'/g, "'");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static sanitizeInputTemplate(template: string, trustLevel: TrustLevel): string {
|
|
35
|
+
// Trusted: no tamplate sanitization
|
|
36
|
+
if (trustLevel === 'trusted') {
|
|
37
|
+
return template;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Always remove scripts and inline event handlers
|
|
41
|
+
template = template
|
|
42
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
43
|
+
.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
44
|
+
|
|
45
|
+
// Strict: remove iframe, object, embed, href, data: protocol
|
|
46
|
+
if (trustLevel === 'strict') {
|
|
47
|
+
template = template
|
|
48
|
+
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
49
|
+
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
|
50
|
+
.replace(/<embed\b[^>]*>/gi, '')
|
|
51
|
+
.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, '')
|
|
52
|
+
.replace(/src\s*=\s*["']data:[^"']*["']/gi, '');
|
|
53
|
+
}
|
|
54
|
+
return template;
|
|
55
|
+
}
|
|
24
56
|
}
|
|
@@ -47,6 +47,22 @@ export class TokenHelper {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Register a new exact token like "@foo" */
|
|
51
|
+
public static registerCustomToken(
|
|
52
|
+
name: string,
|
|
53
|
+
resolver: (el: HTMLElement, ev: Event) => any
|
|
54
|
+
): void {
|
|
55
|
+
this.customResolvers[name] = resolver;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Register a new prefixed token like "@style:" */
|
|
59
|
+
public static registerPrefixedToken(
|
|
60
|
+
prefix: string,
|
|
61
|
+
resolver: (suffix: string, el: HTMLElement) => any
|
|
62
|
+
): void {
|
|
63
|
+
PREFIXED_TOKEN_RESOLVERS[prefix] = resolver;
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
private static resolveExact(token: string, el: HTMLElement, ev: Event): any | null {
|
|
51
67
|
const resolver = EXACT_TOKEN_RESOLVERS[token];
|
|
52
68
|
return resolver ? resolver(el, ev) : null;
|
|
@@ -80,20 +96,4 @@ export class TokenHelper {
|
|
|
80
96
|
}
|
|
81
97
|
return null;
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
/** Register a new exact token like "@foo" */
|
|
85
|
-
public static registerCustomToken(
|
|
86
|
-
name: string,
|
|
87
|
-
resolver: (el: HTMLElement, ev: Event) => any
|
|
88
|
-
) {
|
|
89
|
-
this.customResolvers[name] = resolver;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Register a new prefixed token like "@style:" */
|
|
93
|
-
public static registerPrefixedToken(
|
|
94
|
-
prefix: string,
|
|
95
|
-
resolver: (suffix: string, el: HTMLElement) => any
|
|
96
|
-
) {
|
|
97
|
-
PREFIXED_TOKEN_RESOLVERS[prefix] = resolver;
|
|
98
|
-
}
|
|
99
99
|
}
|
|
@@ -56,20 +56,30 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
56
56
|
normalizeKeys = [],
|
|
57
57
|
defaults = {},
|
|
58
58
|
}: ComponentOptions) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const template = templateFn(normalizedConfig);
|
|
63
|
-
|
|
59
|
+
const template = ConfigHelper.createNormalizedTemplate({
|
|
60
|
+
config, defaults, normalizeKeys, templateFn,
|
|
61
|
+
});
|
|
64
62
|
super({template, mountTarget, tagName});
|
|
65
63
|
|
|
66
|
-
this.config = normalizeKeys
|
|
67
|
-
? ConfigHelper.normalizeStrings(config, normalizeKeys)
|
|
68
|
-
: config ?? ({} as T);
|
|
69
|
-
|
|
64
|
+
this.config = ConfigHelper.setConfigValue(config, normalizeKeys);
|
|
70
65
|
this.props = props;
|
|
66
|
+
this.buttonManager = this.createButtonManager();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public getValues(): Record<string, string> {
|
|
70
|
+
return DataBindingHelper.getDataBindingValues(this.element);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public override render(): HTMLElement {
|
|
74
|
+
this.applyProps();
|
|
75
|
+
this.appendDynamicButtons();
|
|
76
|
+
this.autoBindEvents();
|
|
77
|
+
this.parseDataAttributes();
|
|
78
|
+
return super.render();
|
|
79
|
+
}
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
private createButtonManager(): ButtonManager {
|
|
82
|
+
return new ButtonManager(
|
|
73
83
|
this,
|
|
74
84
|
this.props.buttons,
|
|
75
85
|
{ containerClassName: 'component-buttons' }
|
|
@@ -80,11 +90,9 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
80
90
|
if (this.props.className) {
|
|
81
91
|
this.element.classList.add(this.props.className);
|
|
82
92
|
}
|
|
83
|
-
|
|
84
93
|
if (this.props.style) {
|
|
85
94
|
this.element.style = this.props.style;
|
|
86
95
|
}
|
|
87
|
-
|
|
88
96
|
this.applyDataBindings();
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -95,16 +103,4 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
95
103
|
private applyDataBindings(): void {
|
|
96
104
|
DataBindingHelper.applyDataBindings(this.element, this.props);
|
|
97
105
|
}
|
|
98
|
-
|
|
99
|
-
public getValues(): Record<string, string> {
|
|
100
|
-
return DataBindingHelper.getDataBindingValues(this.element);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public override render(): HTMLElement {
|
|
104
|
-
this.applyProps();
|
|
105
|
-
this.appendDynamicButtons();
|
|
106
|
-
this.autoBindEvents();
|
|
107
|
-
this.parseDataAttributes();
|
|
108
|
-
return super.render();
|
|
109
|
-
}
|
|
110
106
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export class ConfigHelper {
|
|
2
|
-
static normalize<T extends Record<string, any>>(
|
|
2
|
+
public static normalize<T extends Record<string, any>>(
|
|
3
3
|
config: T,
|
|
4
4
|
defaults: Partial<T>
|
|
5
5
|
): T {
|
|
@@ -9,7 +9,7 @@ export class ConfigHelper {
|
|
|
9
9
|
} as T;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
static normalizeStrings<T extends Record<string, any>>(
|
|
12
|
+
public static normalizeStrings<T extends Record<string, any>>(
|
|
13
13
|
config: T,
|
|
14
14
|
stringKeys: (keyof T)[]
|
|
15
15
|
): T {
|
|
@@ -23,4 +23,21 @@ export class ConfigHelper {
|
|
|
23
23
|
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
public static createNormalizedTemplate<T extends Record<string, any>>({
|
|
28
|
+
config,
|
|
29
|
+
defaults = {},
|
|
30
|
+
normalizeKeys = [],
|
|
31
|
+
templateFn = () => ''
|
|
32
|
+
}: T) {
|
|
33
|
+
let normalizedConfig = ConfigHelper.normalize(config, defaults);
|
|
34
|
+
normalizedConfig = ConfigHelper.normalizeStrings(normalizedConfig, normalizeKeys);
|
|
35
|
+
return templateFn(normalizedConfig);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public static setConfigValue<T extends Record<string, any>>(config: T, normalizeKeys: (keyof T)[]): T {
|
|
39
|
+
if (normalizeKeys) return ConfigHelper.normalizeStrings(config, normalizeKeys) as T;
|
|
40
|
+
else if (config) return config as T
|
|
41
|
+
else return ({} as T);
|
|
42
|
+
}
|
|
26
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComponentProps } from "../component.js";
|
|
2
2
|
|
|
3
3
|
export class DataBindingHelper {
|
|
4
|
-
static applyDataBindings(element: HTMLElement, props: ComponentProps): void {
|
|
4
|
+
public static applyDataBindings(element: HTMLElement, props: ComponentProps): void {
|
|
5
5
|
const bindEls = element.querySelectorAll('[data-bind]');
|
|
6
6
|
bindEls.forEach(el => {
|
|
7
7
|
const key = el.getAttribute('data-bind');
|
|
@@ -18,7 +18,7 @@ export class DataBindingHelper {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
static getDataBindingValues(element: HTMLElement): Record<string, string> {
|
|
21
|
+
public static getDataBindingValues(element: HTMLElement): Record<string, string> {
|
|
22
22
|
const values: Record<string, string> = {};
|
|
23
23
|
element.querySelectorAll('[data-bind]').forEach(el => {
|
|
24
24
|
const key = el.getAttribute('data-bind');
|
|
@@ -15,16 +15,10 @@ export abstract class Service<T extends Service<T>> {
|
|
|
15
15
|
`${constructor.name} is a Service. Use ${constructor.name}.getInstance() instead.`
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
|
+
this.autoBindMethods();
|
|
18
19
|
window.addEventListener('beforeunload', this.dispose);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
* For async and custom cleanup operations
|
|
23
|
-
*/
|
|
24
|
-
protected onDestroy<T extends Service<T>>(this: () => T): void {
|
|
25
|
-
console.log(`No onDestroy operations.`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
public static createInstance<T extends Service<T>>(
|
|
29
23
|
constructor: new (...args: any[]) => T,
|
|
30
24
|
...args: any[]
|
|
@@ -62,11 +56,7 @@ export abstract class Service<T extends Service<T>> {
|
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
this._cleanupCallbacks.push(callback);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public dispose = () => {
|
|
59
|
+
public dispose = (): void => {
|
|
70
60
|
this._cleanupCallbacks.forEach(fn => fn());
|
|
71
61
|
this._cleanupCallbacks = [];
|
|
72
62
|
Service._instances.delete(this.constructor);
|
|
@@ -81,6 +71,39 @@ export abstract class Service<T extends Service<T>> {
|
|
|
81
71
|
Service._instantiating.clear();
|
|
82
72
|
}
|
|
83
73
|
|
|
74
|
+
protected registerCleanup(callback: () => void): void {
|
|
75
|
+
this._cleanupCallbacks.push(callback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* For async and custom cleanup operations
|
|
80
|
+
*/
|
|
81
|
+
protected onDestroy<T extends Service<T>>(this: () => T): void {
|
|
82
|
+
console.log(`No onDestroy operations.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private autoBindMethods(): void {
|
|
86
|
+
let proto = Object.getPrototypeOf(this);
|
|
87
|
+
|
|
88
|
+
while (proto && proto !== Service.prototype) {
|
|
89
|
+
const propertyNames = Object.getOwnPropertyNames(proto);
|
|
90
|
+
|
|
91
|
+
for (const key of propertyNames) {
|
|
92
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
key !== 'constructor' &&
|
|
96
|
+
descriptor &&
|
|
97
|
+
typeof descriptor.value === 'function'
|
|
98
|
+
) {
|
|
99
|
+
(this as any)[key] = descriptor.value.bind(this);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
proto = Object.getPrototypeOf(proto);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
/**
|
|
85
108
|
* Resets the Service instance without cleanup.
|
|
86
109
|
*/
|
|
@@ -18,12 +18,6 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
|
|
|
18
18
|
this.viewName = viewName || this.getKebabCaseViewName();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
private getKebabCaseViewName(): string {
|
|
22
|
-
const className = this.constructor.name;
|
|
23
|
-
const baseName = className.replace(/View$/, '');
|
|
24
|
-
return baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
21
|
/**
|
|
28
22
|
* Set route parameters for this view instance
|
|
29
23
|
* Called by the router when the view is instantiated
|
|
@@ -74,4 +68,10 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
|
|
|
74
68
|
super.destroy();
|
|
75
69
|
this.onExit();
|
|
76
70
|
}
|
|
71
|
+
|
|
72
|
+
private getKebabCaseViewName(): string {
|
|
73
|
+
const className = this.constructor.name;
|
|
74
|
+
const baseName = className.replace(/View$/, '');
|
|
75
|
+
return baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
76
|
+
}
|
|
77
77
|
}
|
|
@@ -34,18 +34,6 @@ export class EventBus extends Service<EventBus> {
|
|
|
34
34
|
this.addHandler(event, callback, true);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
private addHandler<K extends EventKey>(
|
|
38
|
-
event: K,
|
|
39
|
-
callback: (data: EventMap[K]) => void,
|
|
40
|
-
once: boolean
|
|
41
|
-
) {
|
|
42
|
-
if (!this.handlers[event]) {
|
|
43
|
-
this.handlers[event] = [];
|
|
44
|
-
}
|
|
45
|
-
this.handlers[event]!.push(callback);
|
|
46
|
-
this._subscriptions.push({ event, callback, once });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
public emit<K extends EventKey>(event: K, data?: EventMap[K]): void {
|
|
50
38
|
const callbacks = this.handlers[event];
|
|
51
39
|
if (!callbacks) return;
|
|
@@ -80,7 +68,19 @@ export class EventBus extends Service<EventBus> {
|
|
|
80
68
|
);
|
|
81
69
|
}
|
|
82
70
|
|
|
83
|
-
private
|
|
71
|
+
private addHandler<K extends EventKey>(
|
|
72
|
+
event: K,
|
|
73
|
+
callback: (data: EventMap[K]) => void,
|
|
74
|
+
once: boolean
|
|
75
|
+
): void {
|
|
76
|
+
if (!this.handlers[event]) {
|
|
77
|
+
this.handlers[event] = [];
|
|
78
|
+
}
|
|
79
|
+
this.handlers[event]!.push(callback);
|
|
80
|
+
this._subscriptions.push({ event, callback, once });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private cleanupEventListeners = (): void => {
|
|
84
84
|
this._subscriptions.forEach(({ event, callback }) => {
|
|
85
85
|
this.off(event, callback);
|
|
86
86
|
});
|
|
@@ -12,11 +12,21 @@ export interface IRequestConfig {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class HttpBuilder {
|
|
15
|
-
static
|
|
16
|
-
|
|
15
|
+
public static buildRequestOptions(
|
|
16
|
+
method: HttpMethod,
|
|
17
|
+
data: unknown,
|
|
18
|
+
config: IRequestConfig,
|
|
19
|
+
signal: AbortSignal
|
|
20
|
+
): RequestInit {
|
|
21
|
+
return {
|
|
22
|
+
method,
|
|
23
|
+
headers: config.headers,
|
|
24
|
+
body: this.buildRequestBody(data),
|
|
25
|
+
signal
|
|
26
|
+
};
|
|
17
27
|
}
|
|
18
28
|
|
|
19
|
-
static buildRequestUrl(endpoint: string, queryParams?: Record<string, string>): URL {
|
|
29
|
+
public static buildRequestUrl(endpoint: string, queryParams?: Record<string, string>): URL {
|
|
20
30
|
const url = new URL(endpoint);
|
|
21
31
|
|
|
22
32
|
if (queryParams) {
|
|
@@ -26,23 +36,13 @@ export class HttpBuilder {
|
|
|
26
36
|
return url;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
static
|
|
39
|
+
private static buildRequestBody(data: unknown): string | undefined {
|
|
40
|
+
return data ? JSON.stringify(data) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static appendQueryParams(url: URL, queryParams: Record<string, string>): void {
|
|
30
44
|
Object.entries(queryParams).forEach(([key, value]) => {
|
|
31
45
|
url.searchParams.append(key, value);
|
|
32
46
|
});
|
|
33
47
|
}
|
|
34
|
-
|
|
35
|
-
static buildRequestOptions(
|
|
36
|
-
method: HttpMethod,
|
|
37
|
-
data: unknown,
|
|
38
|
-
config: IRequestConfig,
|
|
39
|
-
signal: AbortSignal
|
|
40
|
-
): RequestInit {
|
|
41
|
-
return {
|
|
42
|
-
method,
|
|
43
|
-
headers: config.headers,
|
|
44
|
-
body: this.buildRequestBody(data),
|
|
45
|
-
signal
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
48
|
}
|
|
@@ -16,7 +16,7 @@ export class HttpError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export class HttpManager {
|
|
19
|
-
static createAbortController(timeout?: number): IAbortControllerSetup {
|
|
19
|
+
public static createAbortController(timeout?: number): IAbortControllerSetup {
|
|
20
20
|
const controller = new AbortController();
|
|
21
21
|
const timeoutId = timeout ?
|
|
22
22
|
setTimeout(() => controller.abort(), timeout) :
|
|
@@ -25,25 +25,13 @@ export class HttpManager {
|
|
|
25
25
|
return { controller, timeoutId };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
static async
|
|
29
|
-
try {
|
|
30
|
-
return await response.json();
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
static async validateResponse(response: Response): Promise<void> {
|
|
28
|
+
public static async validateResponse(response: Response): Promise<void> {
|
|
37
29
|
if (!response.ok) {
|
|
38
30
|
await this.handleResponseError(response);
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
|
-
|
|
42
|
-
static isJsonResponse(contentType: string | null): boolean {
|
|
43
|
-
return contentType?.includes('application/json') ?? false;
|
|
44
|
-
}
|
|
45
33
|
|
|
46
|
-
static async parseSuccessResponse<T>(response: Response): Promise<T> {
|
|
34
|
+
public static async parseSuccessResponse<T>(response: Response): Promise<T> {
|
|
47
35
|
const contentType = response.headers.get('content-type');
|
|
48
36
|
|
|
49
37
|
if (this.isJsonResponse(contentType)) {
|
|
@@ -53,25 +41,37 @@ export class HttpManager {
|
|
|
53
41
|
return await response.text() as unknown as T;
|
|
54
42
|
}
|
|
55
43
|
|
|
56
|
-
static
|
|
57
|
-
return error instanceof DOMException && error.name === 'AbortError';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static handleRequestError(error: unknown): never {
|
|
44
|
+
public static handleRequestError(error: unknown): never {
|
|
61
45
|
if (this.isTimeoutError(error)) {
|
|
62
46
|
throw new Error('Request timed out');
|
|
63
47
|
}
|
|
64
48
|
throw error;
|
|
65
49
|
}
|
|
66
50
|
|
|
67
|
-
static
|
|
51
|
+
public static cleanupTimeout(timeoutId: TimeoutType | null): void {
|
|
52
|
+
if (timeoutId) {
|
|
53
|
+
clearTimeout(timeoutId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static async handleResponseError(response: Response): Promise<never> {
|
|
68
58
|
const errorData = await this.safeParseErrorResponse(response);
|
|
69
59
|
throw new HttpError(response.status, response.statusText, errorData);
|
|
70
60
|
}
|
|
71
61
|
|
|
72
|
-
static
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
private static isJsonResponse(contentType: string | null): boolean {
|
|
63
|
+
return contentType?.includes('application/json') ?? false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static async safeParseErrorResponse(response: Response): Promise<unknown> {
|
|
67
|
+
try {
|
|
68
|
+
return await response.json();
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
75
71
|
}
|
|
76
72
|
}
|
|
73
|
+
|
|
74
|
+
private static isTimeoutError(error: unknown): boolean {
|
|
75
|
+
return error instanceof DOMException && error.name === 'AbortError';
|
|
76
|
+
}
|
|
77
77
|
}
|
|
@@ -17,15 +17,31 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
17
17
|
this._defaultHeaders = { 'Content-Type': 'application/json', ...defaultHeaders };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
public
|
|
20
|
+
public get<T = unknown>(endpoint: string, config?: IRequestConfig): Promise<T> {
|
|
21
|
+
return this.request<T>('GET', endpoint, null, config);
|
|
22
|
+
}
|
|
23
|
+
public post<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
24
|
+
return this.request<T>('POST', endpoint, data, config);
|
|
25
|
+
}
|
|
26
|
+
public put<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
27
|
+
return this.request<T>('PUT', endpoint, data, config);
|
|
28
|
+
}
|
|
29
|
+
public patch<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
30
|
+
return this.request<T>('PATCH', endpoint, data, config);
|
|
31
|
+
}
|
|
32
|
+
public delete<T = unknown>(endpoint: string, config?: IRequestConfig): Promise<T> {
|
|
33
|
+
return this.request<T>('DELETE', endpoint, undefined, config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public addRequestInterceptor(fn: (url: string, options: RequestInit) => void): void {
|
|
21
37
|
this._requestInterceptors.push(fn);
|
|
22
38
|
}
|
|
23
39
|
|
|
24
|
-
public addResponseInterceptor(fn: (response: Response) => void) {
|
|
40
|
+
public addResponseInterceptor(fn: (response: Response) => void): void {
|
|
25
41
|
this._responseInterceptors.push(fn);
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
onDestroy(): void {
|
|
44
|
+
protected onDestroy(): void {
|
|
29
45
|
this._baseUrl = '';
|
|
30
46
|
this._defaultHeaders = {};
|
|
31
47
|
this._requestInterceptors = [];
|
|
@@ -60,22 +76,6 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
60
76
|
HttpManager.cleanupTimeout(timeoutId);
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
|
-
|
|
64
|
-
public get<T = unknown>(endpoint: string, config?: IRequestConfig) {
|
|
65
|
-
return this.request<T>('GET', endpoint, null, config);
|
|
66
|
-
}
|
|
67
|
-
public post<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
68
|
-
return this.request<T>('POST', endpoint, data, config);
|
|
69
|
-
}
|
|
70
|
-
public put<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
71
|
-
return this.request<T>('PUT', endpoint, data, config);
|
|
72
|
-
}
|
|
73
|
-
public patch<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
74
|
-
return this.request<T>('PATCH', endpoint, data, config);
|
|
75
|
-
}
|
|
76
|
-
public delete<T = unknown>(endpoint: string, config?: IRequestConfig) {
|
|
77
|
-
return this.request<T>('DELETE', endpoint, undefined, config);
|
|
78
|
-
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export const AppHttpClient = HttpClient.getInstance();
|
|
@@ -7,11 +7,7 @@ class PipeRegistry extends Service<PipeRegistry> {
|
|
|
7
7
|
|
|
8
8
|
constructor() { super(); }
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
this._pipes = {};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
register(name: string, fn: PipeFunction): void {
|
|
10
|
+
public register(name: string, fn: PipeFunction): void {
|
|
15
11
|
if(this._pipes[name]) {
|
|
16
12
|
console.warn(`Skipping pipe "${name}": A pipe with this name already exists.`);
|
|
17
13
|
return;
|
|
@@ -19,7 +15,7 @@ class PipeRegistry extends Service<PipeRegistry> {
|
|
|
19
15
|
this._pipes[name] = fn;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
apply(name: string, value: any, args: string[] = []): string {
|
|
18
|
+
public apply(name: string, value: any, args: string[] = []): string {
|
|
23
19
|
const pipe = this._pipes[name];
|
|
24
20
|
if (!pipe) {
|
|
25
21
|
console.warn(`Pipe "${name}" not found.`);
|
|
@@ -27,6 +23,10 @@ class PipeRegistry extends Service<PipeRegistry> {
|
|
|
27
23
|
}
|
|
28
24
|
return pipe(value, ...args);
|
|
29
25
|
}
|
|
26
|
+
|
|
27
|
+
protected onDestroy(): void {
|
|
28
|
+
this._pipes = {};
|
|
29
|
+
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const AppPipeRegistry = PipeRegistry.getInstance();
|