@nutin/cli 1.2.3 → 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 (69) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +23 -15
  3. package/lib/index.mjs +7 -4
  4. package/lib/src/context-builder.mjs +35 -0
  5. package/lib/src/file-generator.mjs +26 -126
  6. package/lib/src/git-manager.mjs +1 -1
  7. package/lib/src/json-generator.mjs +31 -0
  8. package/lib/src/package-manager.mjs +21 -18
  9. package/lib/src/project-generator.mjs +53 -0
  10. package/lib/src/prompts.mjs +28 -15
  11. package/lib/src/template-compiler.mjs +1 -1
  12. package/lib/src/utils.mjs +9 -9
  13. package/lib/src/version.mjs +1 -0
  14. package/package.json +1 -1
  15. package/templates/base/.gitignore.hbs +1 -0
  16. package/templates/base/builder.config.js.hbs +7 -0
  17. package/templates/base/src/app/main.ts.hbs +2 -1
  18. package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
  19. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  20. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
  21. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +23 -20
  22. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +2 -2
  23. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
  24. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +26 -13
  25. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +9 -9
  26. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
  27. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
  28. package/templates/base/src/core/base-classes/component/component.ts.hbs +12 -12
  29. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +2 -2
  30. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
  31. package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
  32. package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
  33. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
  34. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
  35. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
  36. package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
  37. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
  38. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +13 -13
  39. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
  40. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +6 -5
  41. package/templates/base/src/core/services/router/router.ts.hbs +45 -45
  42. package/templates/base/src/core/services/store/store.ts.hbs +6 -6
  43. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +6 -8
  44. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +9 -11
  45. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
  46. package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
  47. package/templates/base/src/libs/popover/popover.ts.hbs +48 -48
  48. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +1 -1
  49. package/templates/base/tools/builder/builder.js.hbs +9 -3
  50. package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
  51. package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
  52. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
  53. package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
  54. package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
  55. package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
  56. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
  57. package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
  58. package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
  59. package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
  60. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +38 -47
  61. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +17 -0
  62. package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
  63. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
  64. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
  65. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
  66. package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
  67. package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
  68. package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
  69. /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
package/lib/src/utils.mjs CHANGED
@@ -5,14 +5,14 @@ import { print } from './print.mjs';
5
5
  export const promiseExec = promisify(exec);
6
6
 
7
7
  export function displaySuccessMessage(answers) {
8
- const { projectName } = answers;
9
-
8
+ const { projectName, packageManager, deployHelper } = answers;
10
9
  print.boldSuccess('\n🎉 Your project is ready!');
11
- print.info('\nNext steps:');
12
- print.info(` cd ${projectName} && npm run serve`);
13
- print.boldInfo(`\nDocumentation on https://www.nutin.org`);
14
-
15
- print.success('\nHappy coding! 🚀\n');
16
- }
10
+ print.boldInfo(`Documentation on https://www.nutin.org`);
11
+
12
+ if (packageManager === 'pnpm' || packageManager === 'bun') {
13
+ if (deployHelper) print.boldError(`You'll need to modify the Dockerfile to support ${packageManager}`)
14
+ }
17
15
 
18
- export const packageVersion = '1.2.1';
16
+ print.info('Next steps:');
17
+ print.info(` cd ${projectName} && ${packageManager} run serve\n`);
18
+ }
@@ -0,0 +1 @@
1
+ export const packageVersion = '1.3.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nutin/cli",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Build SPAs by understanding SPAs. A minimal frontend toolkit crafted for learning and lean applications.",
5
5
  "preferGlobal": true,
6
6
  "main": "lib/index.mjs",
@@ -6,6 +6,7 @@ pnpm-debug.log*
6
6
  .yarn-integrity
7
7
 
8
8
  /dist/
9
+ /dist-build/
9
10
 
10
11
  .DS_Store
11
12
  .DS_Store?
@@ -24,5 +24,12 @@ export default {
24
24
  {{/if}}
25
25
  ], // in styles/
26
26
  },
27
+ {{#if deployHelper}}
28
+ compression: {
29
+ gzip: true,
30
+ // BROTLI OPTIONAL
31
+ // brotli: false,
32
+ },
33
+ {{/if}}
27
34
  verbose: false,
28
35
  }
@@ -22,6 +22,7 @@ class App {
22
22
  }
23
23
 
24
24
  private renderBaseLayout() {
25
+ // e
25
26
  // document.body.prepend(this.header.render());
26
27
  // document.body.append(this.footer.render());
27
28
  }
@@ -36,5 +37,5 @@ document.addEventListener('DOMContentLoaded', async () => {
36
37
  });
37
38
 
38
39
  window.addEventListener('beforeunload', async () => {
39
- Service.destroyAll();
40
+ await Service.destroyAll();
40
41
  });
@@ -0,0 +1,26 @@
1
+ import { HomeView } from '#root/dist/src/app/views/index.js';
2
+
3
+ let view;
4
+
5
+ describe('HomeView', () => {
6
+ beforeAll(() => {
7
+ view = new HomeView();
8
+ });
9
+
10
+ it('should be defined', () => {
11
+ expect(view).toBeDefined();
12
+ });
13
+ {{#if i18n}}
14
+ it('should have title with i18n key', () => {
15
+ const h1 = view.element.querySelector('h1');
16
+ expect(h1).toBeDefined();
17
+ expect(h1.textContent).toBe('home.title');
18
+ });
19
+ {{else}}
20
+ it('should have title', () => {
21
+ const h1 = view.element.querySelector('h1');
22
+ expect(h1).toBeDefined();
23
+ expect(h1.textContent).toBe('{{projectName}}');
24
+ });
25
+ {{/if}}
26
+ })
@@ -4,7 +4,7 @@ const template = `{{#if template}}__TEMPLATE_PLACEHOLDER__{{else}}<div
4
4
  {{#if stylinNutin}}
5
5
  class="u-flex-center u-flex-column u-padd-y-large"
6
6
  {{else}}
7
- style="display:flex;justify-content:center;align-items:center;flex-direction:column"
7
+ style="display:flex;justify-content:center;align-items:center;flex-direction:column;padding-top:1rem;padding-bottom: 1rem;"
8
8
  {{/if}}>
9
9
  <h1
10
10
  {{#if i18n}}
@@ -33,7 +33,7 @@ const template = `{{#if template}}__TEMPLATE_PLACEHOLDER__{{else}}<div
33
33
  class="u-font-large u-padd-y-medium"
34
34
  style="margin-top:-2rem;"
35
35
  {{else}}
36
- style="font-size:1.5rem;paddin-top:1rem;padding-bottom:1rem;margin-top:2rem;"
36
+ style="font-size:1.5rem;paddin-top:1rem;padding-bottom:1rem;margin-top:-2rem;"
37
37
  {{/if}}
38
38
  >{{#unless i18n}}You're all set !{{/unless}}</div>
39
39
  <div
@@ -35,7 +35,7 @@ export class NotFoundView extends View {
35
35
  return [
36
36
  {
37
37
  selector: 'home',
38
- factory: (el) => new ButtonComponent(el, { {{#if i18n}}i18nKey: 'not-found.redirect'{{else}}textContent: 'Back to home'{{/if}}, callback: () => this.handleHome(), {{#if stylinNutin}}className: btnClass{{else}}styles: btnStyles{{/if}} })
38
+ factory: (el) => new ButtonComponent(el, { {{#if i18n}}i18nKey: 'not-found.redirect'{{else}}textContent: 'Back to home'{{/if}}, callback: () => this.handleHome(), {{#if stylinNutin}}className: btnClass{{else}}style: btnStyles{{/if}} })
39
39
  },
40
40
  ]
41
41
  }
@@ -6,6 +6,7 @@ import { I18nHelper } from './helpers/i18n.helper.js';
6
6
  import { PipeHelper } from './helpers/pipe.helper.js';
7
7
  import { ChildrenHelper } from './helpers/children.helper.js';
8
8
  import { CatalogHelper, CatalogConfig } from './helpers/catalog.helper.js';
9
+ import { TrustLevel } from './helpers/security.helper.js';
9
10
  export { CatalogItemConfig } from './helpers/catalog.helper.js';
10
11
 
11
12
  /**```typescript
@@ -24,6 +25,7 @@ export interface BaseComponentOptions {
24
25
  template?: string;
25
26
  mountTarget?: string | HTMLElement;
26
27
  tagName?: keyof HTMLElementTagNameMap;
28
+ trustLevel?: TrustLevel;
27
29
  }
28
30
 
29
31
  export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
@@ -34,27 +36,28 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
34
36
  constructor({
35
37
  template = '',
36
38
  mountTarget = '#app',
37
- tagName = 'div'
39
+ tagName = 'div',
40
+ trustLevel
38
41
  }: BaseComponentOptions) {
39
- this.element = DomHelper.createElement<T>(tagName, template);
42
+ this.element = DomHelper.createElement<T>(tagName, template, trustLevel);
40
43
  this.parseDataAttributes();
41
44
  this.autoBindEvents();
42
45
  DomHelper.mountElement(this.element, mountTarget);
43
46
  }
44
47
 
45
- protected parseDataAttributes(): void {
46
- {{#if i18n}}
47
- I18nHelper.parseI18nAttributes(this.element);
48
- {{/if}}
49
- PipeHelper.parsePipeAttributes(this.element);
48
+ public childConfigs(): ComponentConfig[] {
49
+ return [];
50
50
  }
51
51
 
52
- protected autoBindEvents(): void {
53
- EventHelper.bindEvents(this, this.element, this.eventListeners);
52
+ public destroy(): void {
53
+ EventHelper.destroyEvents(this.element, this.eventListeners);
54
+ ChildrenHelper.destroyChildren(this._children);
55
+ this.element.remove();
54
56
  }
55
57
 
56
- protected addChildren(): void {
57
- ChildrenHelper.addChildren(this, this.element, this._children);
58
+ public render(): HTMLElement {
59
+ this.addChildren();
60
+ return this.element;
58
61
  }
59
62
 
60
63
  /**
@@ -72,18 +75,18 @@ interface CatalogConfig {
72
75
  return CatalogHelper.generateCatalog(config);
73
76
  }
74
77
 
75
- public childConfigs(): ComponentConfig[] {
76
- return [];
78
+ protected parseDataAttributes(): void {
79
+ {{#if i18n}}
80
+ I18nHelper.parseI18nAttributes(this.element);
81
+ {{/if}}
82
+ PipeHelper.parsePipeAttributes(this.element);
77
83
  }
78
84
 
79
- public destroy(): void {
80
- EventHelper.destroyEvents(this.element, this.eventListeners);
81
- ChildrenHelper.destroyChildren(this._children);
82
- this.element.remove();
85
+ protected autoBindEvents(): void {
86
+ EventHelper.bindEvents(this, this.element, this.eventListeners);
83
87
  }
84
88
 
85
- public render(): HTMLElement {
86
- this.addChildren();
87
- return this.element;
89
+ protected addChildren(): void {
90
+ ChildrenHelper.addChildren(this, this.element, this._children);
88
91
  }
89
92
  }
@@ -53,14 +53,14 @@ export class CatalogHelper {
53
53
  return componentConfigs;
54
54
  }
55
55
 
56
- private static createElements(index: number, config: CatalogConfig, container: HTMLElement) {
56
+ private static createElements(index: number, config: CatalogConfig, container: HTMLElement): void {
57
57
  const el = document.createElement(config.elementTag || 'div');
58
58
  el.setAttribute('data-component', `${config.elementName}-${index}`);
59
59
  el.dataset.index = String(index);
60
60
  container?.appendChild(el);
61
61
  }
62
62
 
63
- private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig) {
63
+ private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig): void {
64
64
  const item = config.array[index];
65
65
 
66
66
  const configWithIndex: CatalogItemConfig = (item && typeof item === 'object')
@@ -18,11 +18,11 @@ export class ChildrenHelper {
18
18
  });
19
19
  }
20
20
 
21
- private static registerChild(child: BaseComponent, children: BaseComponent[]): void {
22
- children.push(child);
23
- }
24
-
25
21
  public static destroyChildren(children: BaseComponent[]): void {
26
22
  children.forEach(child => child.destroy());
27
23
  }
24
+
25
+ private static registerChild(child: BaseComponent, children: BaseComponent[]): void {
26
+ children.push(child);
27
+ }
28
28
  }
@@ -1,28 +1,41 @@
1
+ import { SecurityHelper, TrustLevel } from "./security.helper.js";
2
+
3
+ interface DomElementConfig {
4
+ target: Element | null;
5
+ mountTarget: string | HTMLElement;
6
+ element: HTMLElement
7
+ }
8
+
1
9
  export class DomHelper {
2
10
  public static mountElement(element: HTMLElement, mountTarget: string | HTMLElement): void {
3
11
  const target = typeof mountTarget === 'string'
4
12
  ? document.querySelector(mountTarget)
5
13
  : mountTarget;
6
14
 
7
- if (target instanceof HTMLElement) {
8
- if (typeof mountTarget === 'string') {
9
- // Append mode
10
- target.appendChild(element);
11
- } else {
12
- // Replace placeholder mode
13
- target.replaceWith(element);
14
- }
15
- } else {
16
- console.warn('Invalid mount target for component:', mountTarget);
17
- }
15
+ this.appendOrReplace({ target, element, mountTarget });
18
16
  }
19
17
 
20
18
  public static createElement<T extends HTMLElement>(
21
19
  tagName: keyof HTMLElementTagNameMap,
22
- template: string = ''
20
+ template: string = '',
21
+ trustLevel?: TrustLevel
23
22
  ): T {
24
23
  const element = document.createElement(tagName) as T;
25
- element.innerHTML = template;
24
+ element.innerHTML = SecurityHelper.sanitizeTemplate(template, trustLevel);
26
25
  return element;
27
26
  }
27
+
28
+ private static appendOrReplace(config: DomElementConfig) {
29
+ if (config.target instanceof HTMLElement) {
30
+ if (typeof config.mountTarget === 'string') {
31
+ // Append mode
32
+ config.target.appendChild(config.element);
33
+ } else {
34
+ // Replace placeholder mode
35
+ config.target.replaceWith(config.element);
36
+ }
37
+ } else {
38
+ console.warn('Mount target not found:', config.mountTarget);
39
+ }
40
+ }
28
41
  }
@@ -23,6 +23,15 @@ export class EventHelper {
23
23
  });
24
24
  }
25
25
 
26
+ public static destroyEvents(
27
+ element: HTMLElement,
28
+ eventListeners: Array<[string, EventListener]>
29
+ ): void {
30
+ eventListeners.forEach(([event, listener]) => {
31
+ element.removeEventListener(event, listener);
32
+ });
33
+ }
34
+
26
35
  private static createBoundHandler(
27
36
  el: Element,
28
37
  component: BaseComponent,
@@ -45,13 +54,4 @@ export class EventHelper {
45
54
  target.addEventListener(event, listener);
46
55
  eventListeners.push([event, listener]);
47
56
  }
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
- });
56
- }
57
57
  }
@@ -1,13 +1,11 @@
1
+ export type TrustLevel = 'strict' | 'normal' | 'trusted';
2
+
1
3
  export class SecurityHelper {
2
- public static escapeHtml(value: unknown): string {
4
+ public static sanitizeTemplate(value: unknown, trustLevel: TrustLevel = 'normal'): string {
3
5
  if (value === null || value === undefined) return '';
4
- const str = String(value);
5
- return str
6
- .replace(/&/g, "&amp;")
7
- .replace(/</g, "&lt;")
8
- .replace(/>/g, "&gt;")
9
- .replace(/"/g, "&quot;")
10
- .replace(/'/g, "&#039;");
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, "&amp;")
28
+ .replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;")
30
+ .replace(/"/g, "&quot;")
31
+ .replace(/'/g, "&#039;");
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
  }
@@ -76,6 +76,18 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
76
76
  );
77
77
  }
78
78
 
79
+ public getValues(): Record<string, string> {
80
+ return DataBindingHelper.getDataBindingValues(this.element);
81
+ }
82
+
83
+ public override render(): HTMLElement {
84
+ this.applyProps();
85
+ this.appendDynamicButtons();
86
+ this.autoBindEvents();
87
+ this.parseDataAttributes();
88
+ return super.render();
89
+ }
90
+
79
91
  private applyProps(): void {
80
92
  if (this.props.className) {
81
93
  this.element.classList.add(this.props.className);
@@ -95,16 +107,4 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
95
107
  private applyDataBindings(): void {
96
108
  DataBindingHelper.applyDataBindings(this.element, this.props);
97
109
  }
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
110
  }
@@ -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 {
@@ -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
- protected registerCleanup(callback: () => void) {
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 cleanupEventListeners = () => {
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
  });