structured-fw 0.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/Config.ts +47 -0
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/app/Types.ts +1 -0
- package/app/models/README.md +9 -0
- package/app/routes/README.md +19 -0
- package/app/views/README.md +1 -0
- package/app/views/layout.html +1 -0
- package/bin/structured +114 -0
- package/build/Config.d.ts +2 -0
- package/build/Config.js +31 -0
- package/build/app/Types.d.ts +1 -0
- package/build/app/Types.js +1 -0
- package/build/app/models/Users.d.ts +0 -0
- package/build/app/models/Users.js +1 -0
- package/build/app/routes/Auth.d.ts +0 -0
- package/build/app/routes/Auth.js +1 -0
- package/build/app/routes/Test.d.ts +2 -0
- package/build/app/routes/Test.js +101 -0
- package/build/app/routes/Todo.d.ts +0 -0
- package/build/app/routes/Todo.js +1 -0
- package/build/app/routes/Upload.d.ts +0 -0
- package/build/app/routes/Upload.js +1 -0
- package/build/app/routes/Validation.d.ts +2 -0
- package/build/app/routes/Validation.js +34 -0
- package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
- package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
- package/build/app/views/components/ClientImport/Export.d.ts +1 -0
- package/build/app/views/components/ClientImport/Export.js +1 -0
- package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
- package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
- package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
- package/build/app/views/components/PassObject/PassObject.js +10 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
- package/build/assets/ts/Export.d.ts +1 -0
- package/build/assets/ts/Export.js +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +3 -0
- package/build/system/Helpers.d.ts +3 -0
- package/build/system/Helpers.js +72 -0
- package/build/system/Symbols.d.ts +3 -0
- package/build/system/Symbols.js +3 -0
- package/build/system/Types.d.ts +171 -0
- package/build/system/Types.js +1 -0
- package/build/system/Util.d.ts +20 -0
- package/build/system/Util.js +336 -0
- package/build/system/client/App.d.ts +7 -0
- package/build/system/client/App.js +8 -0
- package/build/system/client/Client.d.ts +6 -0
- package/build/system/client/Client.js +9 -0
- package/build/system/client/ClientComponent.d.ts +68 -0
- package/build/system/client/ClientComponent.js +734 -0
- package/build/system/client/DataStore.d.ts +22 -0
- package/build/system/client/DataStore.js +64 -0
- package/build/system/client/DataStoreView.d.ts +19 -0
- package/build/system/client/DataStoreView.js +56 -0
- package/build/system/client/EventEmitter.d.ts +7 -0
- package/build/system/client/EventEmitter.js +31 -0
- package/build/system/client/Net.d.ts +13 -0
- package/build/system/client/Net.js +39 -0
- package/build/system/client/NetRequest.d.ts +13 -0
- package/build/system/client/NetRequest.js +45 -0
- package/build/system/server/Application.d.ts +31 -0
- package/build/system/server/Application.js +171 -0
- package/build/system/server/Component.d.ts +27 -0
- package/build/system/server/Component.js +249 -0
- package/build/system/server/Components.d.ts +12 -0
- package/build/system/server/Components.js +77 -0
- package/build/system/server/Cookies.d.ts +6 -0
- package/build/system/server/Cookies.js +19 -0
- package/build/system/server/Document.d.ts +24 -0
- package/build/system/server/Document.js +107 -0
- package/build/system/server/DocumentHead.d.ts +32 -0
- package/build/system/server/DocumentHead.js +118 -0
- package/build/system/server/FormValidation.d.ts +16 -0
- package/build/system/server/FormValidation.js +197 -0
- package/build/system/server/Handlebars.d.ts +11 -0
- package/build/system/server/Handlebars.js +34 -0
- package/build/system/server/Request.d.ts +21 -0
- package/build/system/server/Request.js +356 -0
- package/build/system/server/Session.d.ts +23 -0
- package/build/system/server/Session.js +114 -0
- package/build/system/server/dom/DOMFragment.d.ts +4 -0
- package/build/system/server/dom/DOMFragment.js +6 -0
- package/build/system/server/dom/DOMNode.d.ts +31 -0
- package/build/system/server/dom/DOMNode.js +110 -0
- package/build/system/server/dom/HTMLParser.d.ts +21 -0
- package/build/system/server/dom/HTMLParser.js +204 -0
- package/index.ts +4 -0
- package/package.json +31 -0
- package/system/Helpers.ts +97 -0
- package/system/Symbols.ts +6 -0
- package/system/Types.ts +234 -0
- package/system/Util.ts +488 -0
- package/system/client/App.ts +11 -0
- package/system/client/Client.ts +9 -0
- package/system/client/ClientComponent.ts +1117 -0
- package/system/client/DataStore.ts +101 -0
- package/system/client/DataStoreView.ts +82 -0
- package/system/client/EventEmitter.ts +38 -0
- package/system/client/Net.ts +58 -0
- package/system/client/NetRequest.ts +64 -0
- package/system/server/Application.ts +230 -0
- package/system/server/Component.ts +404 -0
- package/system/server/Components.ts +111 -0
- package/system/server/Cookies.ts +29 -0
- package/system/server/Document.ts +163 -0
- package/system/server/DocumentHead.ts +150 -0
- package/system/server/FormValidation.ts +231 -0
- package/system/server/Handlebars.ts +51 -0
- package/system/server/Request.ts +497 -0
- package/system/server/Session.ts +151 -0
- package/system/server/dom/DOMFragment.ts +7 -0
- package/system/server/dom/DOMNode.ts +140 -0
- package/system/server/dom/HTMLParser.ts +238 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AsteriskAny, StoreChangeCallback } from '../Types.js';
|
|
2
|
+
import { equalDeep } from '../Util.js';
|
|
3
|
+
import { ClientComponent } from './ClientComponent.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class DataStore {
|
|
7
|
+
|
|
8
|
+
protected data: {
|
|
9
|
+
[componentId: string]: {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
12
|
+
} = {};
|
|
13
|
+
|
|
14
|
+
protected changeListeners: {
|
|
15
|
+
[componentId: string]: {
|
|
16
|
+
[key: string]: Array<StoreChangeCallback>;
|
|
17
|
+
};
|
|
18
|
+
} = {};
|
|
19
|
+
|
|
20
|
+
// return self to allow chained calls to set
|
|
21
|
+
public set(component: ClientComponent, key: string, val: any, force: boolean = false, triggerListeners: boolean = true): DataStore {
|
|
22
|
+
const componentId = component.getData<string>('componentId');
|
|
23
|
+
|
|
24
|
+
const oldValue = this.get(componentId, key);
|
|
25
|
+
|
|
26
|
+
if (! force && equalDeep({ value: oldValue }, { value: val })) {
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!this.data[componentId]) {
|
|
31
|
+
this.data[componentId] = {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.data[componentId][key] = val;
|
|
35
|
+
|
|
36
|
+
if (triggerListeners) {
|
|
37
|
+
if (this.changeListeners[componentId] && (this.changeListeners[componentId][key] || this.changeListeners[componentId]['*'])) {
|
|
38
|
+
// there are change listeners, call them
|
|
39
|
+
(this.changeListeners[componentId][key] || []).concat(this.changeListeners[componentId]['*'] || []).forEach((cb) => {
|
|
40
|
+
cb.apply(component, [key, val, oldValue, componentId]);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public get(componentId: string, key?: string): any {
|
|
49
|
+
if (!this.data[componentId]) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (typeof key !== 'string') {
|
|
53
|
+
return this.data[componentId];
|
|
54
|
+
}
|
|
55
|
+
return this.data[componentId][key];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public hasKey(componentId: string, key: string): boolean {
|
|
59
|
+
if (! (componentId in this.data)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return key in this.data[componentId];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// clear data for given componentId
|
|
67
|
+
public clear(componentId: string): void {
|
|
68
|
+
this.data[componentId] = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// clear data and unbind onChange listeners for given componentId
|
|
72
|
+
public destroy(componentId: string): void {
|
|
73
|
+
this.unbindAll(componentId);
|
|
74
|
+
this.clear(componentId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// add callback to be called when a given key's value is changed
|
|
78
|
+
// if key === '*' then it will be called when any of the key's values is changed
|
|
79
|
+
public onChange(componentId: string, key: string | AsteriskAny, callback: StoreChangeCallback): DataStore {
|
|
80
|
+
if (! (componentId in this.changeListeners)) {
|
|
81
|
+
this.changeListeners[componentId] = {};
|
|
82
|
+
}
|
|
83
|
+
if (! (key in this.changeListeners[componentId])) {
|
|
84
|
+
this.changeListeners[componentId][key] = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.changeListeners[componentId][key].push(callback);
|
|
88
|
+
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// return all on change callbacks for given component
|
|
93
|
+
public onChangeCallbacks(componentId: string): Record<string, Array<StoreChangeCallback>> {
|
|
94
|
+
return this.changeListeners[componentId];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// unbind all onChange listeners for given component id
|
|
98
|
+
private unbindAll(componentId: string): void {
|
|
99
|
+
delete this.changeListeners[componentId];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AsteriskAny, StoreChangeCallback } from '../Types.js';
|
|
2
|
+
import { ClientComponent } from './ClientComponent.js';
|
|
3
|
+
import { DataStore } from './DataStore.js';
|
|
4
|
+
|
|
5
|
+
// Simplifies the use of data store
|
|
6
|
+
// it is initialized with component ID and global store so that from component
|
|
7
|
+
// one can set/get a value without having to pass in a component id
|
|
8
|
+
|
|
9
|
+
export class DataStoreView {
|
|
10
|
+
|
|
11
|
+
private store: DataStore;
|
|
12
|
+
private component: ClientComponent;
|
|
13
|
+
private destroyed = false;
|
|
14
|
+
|
|
15
|
+
constructor(store: DataStore, component: ClientComponent) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.component = component;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public set(key: string, val: any, force: boolean = false, triggerListeners: boolean = true): DataStoreView {
|
|
21
|
+
if (! this.destroyed) {
|
|
22
|
+
this.store.set(this.component, key, val, force, triggerListeners);
|
|
23
|
+
}
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public get<T>(key: string): T|undefined {
|
|
28
|
+
if (this.destroyed) {return undefined;}
|
|
29
|
+
return this.store.get(this.componentId(), key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public toggle(key: string): void {
|
|
33
|
+
this.set(key, !this.get(key));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public keys(): Array<string> {
|
|
37
|
+
if (this.destroyed) {return [];}
|
|
38
|
+
return Object.keys(this.store.get(this.componentId()));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// import this.component.data to store
|
|
42
|
+
// existing keys are skipped unless force = true
|
|
43
|
+
public import(fields?: Array<string>, force: boolean = false, triggerListeners: boolean = true): void {
|
|
44
|
+
const fieldsImported = Array.isArray(fields) ? fields : Object.keys(this.component.getData());
|
|
45
|
+
fieldsImported.forEach((field) => {
|
|
46
|
+
// skip existing key, unless force = true
|
|
47
|
+
if (force || ! this.store.hasKey(this.componentId(), field)) {
|
|
48
|
+
this.set(field, this.component.getData(field), force, triggerListeners);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// clear data for owner component
|
|
54
|
+
public clear(): void {
|
|
55
|
+
this.store.clear(this.componentId());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// clear data and unbind onChange listeners for owner component
|
|
59
|
+
// mark this instance as destroyed so it no longer accepts any input
|
|
60
|
+
public destroy(): void {
|
|
61
|
+
this.store.destroy(this.componentId());
|
|
62
|
+
this.destroyed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// return owner component id
|
|
66
|
+
private componentId(): string {
|
|
67
|
+
return this.component.getData<string>('componentId');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// add callback to be called when a given key's value is changed
|
|
71
|
+
// if key === '*' then it will be called when any of the key's values is changed
|
|
72
|
+
public onChange(key: string | AsteriskAny, callback: StoreChangeCallback): DataStoreView {
|
|
73
|
+
if (this.destroyed) {return this;}
|
|
74
|
+
this.store.onChange(this.componentId(), key, callback);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// return all onChange listeners for the owner component
|
|
79
|
+
public onChangeCallbacks(): Record<string, Array<StoreChangeCallback>> {
|
|
80
|
+
return this.store.onChangeCallbacks(this.componentId());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EventEmitterCallback } from "../Types.js";
|
|
2
|
+
|
|
3
|
+
export class EventEmitter {
|
|
4
|
+
protected listeners: Record<string, Array<EventEmitterCallback>> = {}
|
|
5
|
+
|
|
6
|
+
// add event listener
|
|
7
|
+
public on(eventName: string, callback: EventEmitterCallback): void {
|
|
8
|
+
if (! Array.isArray(this.listeners[eventName])) {
|
|
9
|
+
this.listeners[eventName] = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
this.listeners[eventName].push(callback);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// emit event with given payload
|
|
16
|
+
public emit(eventName: string, payload?: any): void {
|
|
17
|
+
if (Array.isArray(this.listeners[eventName])) {
|
|
18
|
+
this.listeners[eventName].forEach((callback) => {
|
|
19
|
+
callback(payload);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// remove event listener
|
|
25
|
+
public unbind(eventName: string, callback: EventEmitterCallback): void {
|
|
26
|
+
if (Array.isArray(this.listeners[eventName])) {
|
|
27
|
+
while (true) {
|
|
28
|
+
const index = this.listeners[eventName].indexOf(callback);
|
|
29
|
+
if (index > -1) {
|
|
30
|
+
this.listeners[eventName].splice(index, 1);
|
|
31
|
+
} else {
|
|
32
|
+
// callback not found, all removed
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import { LooseObject, RequestMethod } from '../Types.js';
|
|
3
|
+
import { NetRequest } from './NetRequest.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class Net {
|
|
7
|
+
|
|
8
|
+
// Make a HTTP request
|
|
9
|
+
public async request(method: RequestMethod, url: string, headers: IncomingHttpHeaders = {}, body?: any, responseType: XMLHttpRequestResponseType = 'text'): Promise<string> {
|
|
10
|
+
const request = new NetRequest(method, url, headers, responseType);
|
|
11
|
+
return request.send(body);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async get(url: string, headers: IncomingHttpHeaders = {}): Promise<string> {
|
|
15
|
+
return this.request('GET', url, headers);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async delete(url: string, headers: IncomingHttpHeaders = {}): Promise<string> {
|
|
19
|
+
return this.request('DELETE', url, headers);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public async post(url: string, data: any, headers: IncomingHttpHeaders = {}): Promise<string> {
|
|
23
|
+
if (typeof data === 'object' && !headers['content-type'] && !(data instanceof FormData)) {
|
|
24
|
+
// if data is object and no content/type header is specified default to application/json
|
|
25
|
+
headers['content-type'] = 'application/json';
|
|
26
|
+
// convert data to JSON
|
|
27
|
+
data = JSON.stringify(data);
|
|
28
|
+
}
|
|
29
|
+
return await this.request('POST', url, headers, data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async put(url: string, data: any, headers: IncomingHttpHeaders = {}): Promise<string> {
|
|
33
|
+
if (typeof data === 'object' && !headers['content-type']) {
|
|
34
|
+
// if data is object and no content/type header is specified default to application/json
|
|
35
|
+
headers['content-type'] = 'application/json';
|
|
36
|
+
// convert data to JSON
|
|
37
|
+
data = JSON.stringify(data);
|
|
38
|
+
}
|
|
39
|
+
return this.request('PUT', url, headers, data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async getJSON<T>(url: string, headers?: IncomingHttpHeaders): Promise<T> {
|
|
43
|
+
return JSON.parse(await this.get(url, headers));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async postJSON<T>(url: string, data: LooseObject, headers?: IncomingHttpHeaders): Promise<T> {
|
|
47
|
+
return JSON.parse(await this.post(url, data, headers));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async deleteJSON<T>(url: string, headers?: IncomingHttpHeaders): Promise<T> {
|
|
51
|
+
return JSON.parse(await this.delete(url, headers));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async putJSON<T>(url: string, data: LooseObject, headers?: IncomingHttpHeaders): Promise<T> {
|
|
55
|
+
return JSON.parse(await this.put(url, data, headers));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import { RequestMethod } from '../Types.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class NetRequest {
|
|
6
|
+
xhr: XMLHttpRequest = new XMLHttpRequest();
|
|
7
|
+
method: RequestMethod;
|
|
8
|
+
url: string;
|
|
9
|
+
headers: IncomingHttpHeaders;
|
|
10
|
+
responseType: XMLHttpRequestResponseType;
|
|
11
|
+
body: any;
|
|
12
|
+
requestSent: boolean = false;
|
|
13
|
+
|
|
14
|
+
constructor(method: RequestMethod, url: string, headers: IncomingHttpHeaders = {}, responseType: XMLHttpRequestResponseType = 'text', body?: any) {
|
|
15
|
+
this.method = method;
|
|
16
|
+
this.url = url;
|
|
17
|
+
this.headers = headers;
|
|
18
|
+
this.responseType = responseType;
|
|
19
|
+
this.body = body;
|
|
20
|
+
|
|
21
|
+
this.xhr.open(this.method, this.url);
|
|
22
|
+
this.xhr.responseType = this.responseType;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// set the X-Requested-With: xmlhttprequest header if not set by user
|
|
26
|
+
if (!('x-requested-with' in headers)) {
|
|
27
|
+
headers['x-requested-with'] = 'xmlhttprequest';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// set request headers
|
|
31
|
+
for (const header in headers) {
|
|
32
|
+
const headerValue = headers[header];
|
|
33
|
+
if (typeof headerValue === 'string') {
|
|
34
|
+
this.xhr.setRequestHeader(header, headerValue);
|
|
35
|
+
} else {
|
|
36
|
+
console.warn('Only string header values are supported');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async send(body?: any): Promise<string> {
|
|
42
|
+
if (this.requestSent) { return ''; }
|
|
43
|
+
this.requestSent = true;
|
|
44
|
+
if (typeof body !== 'undefined') {
|
|
45
|
+
this.body = body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
// listen for state change
|
|
50
|
+
this.xhr.onreadystatechange = () => {
|
|
51
|
+
if (this.xhr.readyState == 4) {
|
|
52
|
+
// got the response
|
|
53
|
+
resolve(this.xhr.responseText);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// reject on error
|
|
57
|
+
this.xhr.onerror = (err) => {
|
|
58
|
+
reject(err);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.xhr.send(body);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { createServer, Server } from 'node:http';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as mime from 'mime-types';
|
|
6
|
+
import { ApplicationEvents, LooseObject, RequestBodyArguments, RequestCallback, RequestContext, StructuredConfig } from '../Types';
|
|
7
|
+
import { Document } from './Document.js';
|
|
8
|
+
import { Components } from './Components.js';
|
|
9
|
+
import { Session } from './Session.js';
|
|
10
|
+
import { toSnakeCase } from '../Util.js';
|
|
11
|
+
import { Request } from './Request.js';
|
|
12
|
+
import { Handlebars } from './Handlebars.js';
|
|
13
|
+
import { Cookies } from './Cookies.js';
|
|
14
|
+
import { RequestContextData } from '../../app/Types.js';
|
|
15
|
+
|
|
16
|
+
export class Application {
|
|
17
|
+
readonly config: StructuredConfig;
|
|
18
|
+
|
|
19
|
+
server: null|Server = null;
|
|
20
|
+
listening: boolean = false;
|
|
21
|
+
|
|
22
|
+
private readonly eventEmitter: EventEmitter = new EventEmitter();
|
|
23
|
+
|
|
24
|
+
readonly cookies: Cookies;
|
|
25
|
+
readonly session: Session;
|
|
26
|
+
readonly request: Request;
|
|
27
|
+
readonly components: Components;
|
|
28
|
+
|
|
29
|
+
// handlebars helpers manager
|
|
30
|
+
readonly handlebars: Handlebars = new Handlebars();
|
|
31
|
+
|
|
32
|
+
// fields from RequestContext.data to be exported for all components
|
|
33
|
+
readonly exportedRequestContextData: Array<keyof RequestContextData> = [];
|
|
34
|
+
|
|
35
|
+
constructor(config: StructuredConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
|
|
38
|
+
this.cookies = new Cookies();
|
|
39
|
+
this.session = new Session(this);
|
|
40
|
+
this.request = new Request(this);
|
|
41
|
+
this.components = new Components(this);
|
|
42
|
+
|
|
43
|
+
// enable sessions
|
|
44
|
+
this.session.start();
|
|
45
|
+
|
|
46
|
+
if (this.config.autoInit) {
|
|
47
|
+
this.init();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async init(): Promise<void> {
|
|
52
|
+
|
|
53
|
+
// max listeners per event
|
|
54
|
+
this.eventEmitter.setMaxListeners(10);
|
|
55
|
+
|
|
56
|
+
// load handlebars helpers
|
|
57
|
+
try {
|
|
58
|
+
await this.handlebars.loadHelpers('../Helpers.js');
|
|
59
|
+
} catch(e) {
|
|
60
|
+
console.error(e.message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.emit('beforeComponentLoad');
|
|
64
|
+
this.components.loadComponents();
|
|
65
|
+
await this.emit('afterComponentLoad');
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
await this.emit('beforeRoutes');
|
|
69
|
+
await this.request.loadHandlers();
|
|
70
|
+
await this.emit('afterRoutes');
|
|
71
|
+
|
|
72
|
+
if (this.config.url.componentRender !== false) {
|
|
73
|
+
// special request handler, executed when ClientComponent.redraw is called
|
|
74
|
+
this.request.on('POST', `${this.config.url.componentRender}`, async (ctx) => {
|
|
75
|
+
const input = ctx.body as unknown as {
|
|
76
|
+
component: string,
|
|
77
|
+
attributes: RequestBodyArguments,
|
|
78
|
+
unwrap?: boolean
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await this.respondWithComponent(ctx, input.component, input.attributes || undefined, typeof input.unwrap === 'boolean' ? input.unwrap : true);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// special request handler, serve the client side JS
|
|
86
|
+
this.request.on('GET', /^\/assets\/client-js/, async ({ request, response }) => {
|
|
87
|
+
const uri = request.url?.substring(18) as string;
|
|
88
|
+
const filePath = path.resolve('./system/', uri);
|
|
89
|
+
if (existsSync(filePath)) {
|
|
90
|
+
response.setHeader('Content-Type', 'application/javascript');
|
|
91
|
+
response.write(readFileSync(filePath));
|
|
92
|
+
response.end();
|
|
93
|
+
} else {
|
|
94
|
+
response.statusCode = 404;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}, this, true);
|
|
98
|
+
|
|
99
|
+
await this.start();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// start the http server
|
|
103
|
+
public start(): Promise<void> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
this.server = createServer((req, res) => {
|
|
106
|
+
this.request.handle(req, res);
|
|
107
|
+
});
|
|
108
|
+
this.server.listen(this.config.http.port, this.config.http.host || '127.0.0.1', async () => {
|
|
109
|
+
const address = (this.config.http.host !== undefined ? this.config.http.host : '') + ':' + this.config.http.port;
|
|
110
|
+
await this.emit('serverStarted');
|
|
111
|
+
console.log(`Server started on ${address}`);
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// add event listener
|
|
118
|
+
public on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void)): void {
|
|
119
|
+
this.eventEmitter.on(evt, callback);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// emit an event on Application
|
|
123
|
+
// this will run all event listeners attached to given eventName
|
|
124
|
+
// providing the payload as the first argument
|
|
125
|
+
// returns an array of all resolved values, any rejected promise values are discarded
|
|
126
|
+
public async emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>> {
|
|
127
|
+
const promises: Array<Promise<any>> = [];
|
|
128
|
+
const listeners = this.eventEmitter.rawListeners(eventName);
|
|
129
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
130
|
+
promises.push(listeners[i](payload));
|
|
131
|
+
}
|
|
132
|
+
const results = await Promise.allSettled(promises);
|
|
133
|
+
return results.filter((res) => {
|
|
134
|
+
return res.status === 'fulfilled';
|
|
135
|
+
}).map((res) => {
|
|
136
|
+
return res.value;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// load envirnment variables
|
|
141
|
+
// if this.config.envPrefix is a string, load all ENV variables starting with [envPrefix]_
|
|
142
|
+
// the method is generic, so user can define the expected return type
|
|
143
|
+
public importEnv<T extends LooseObject>(smartPrimitives: boolean = true): T {
|
|
144
|
+
const values: LooseObject = {}
|
|
145
|
+
const usePrefix = typeof this.config.envPrefix === 'string';
|
|
146
|
+
const prefixLength = usePrefix ? this.config.envPrefix.length : 0;
|
|
147
|
+
for (const key in process.env) {
|
|
148
|
+
if (! usePrefix || key.startsWith(this.config.envPrefix)) {
|
|
149
|
+
// import
|
|
150
|
+
let value: any = process.env[key];
|
|
151
|
+
const keyWithoutPrefix = key.substring(prefixLength + 1);
|
|
152
|
+
|
|
153
|
+
if (smartPrimitives) {
|
|
154
|
+
if (value === 'undefined') {
|
|
155
|
+
value = undefined;
|
|
156
|
+
} else if (value === 'null') {
|
|
157
|
+
value = null;
|
|
158
|
+
} else if (value === 'true') {
|
|
159
|
+
value = true;
|
|
160
|
+
} else if (value === 'false') {
|
|
161
|
+
value = false;
|
|
162
|
+
} else if (/^-?\d+$/.test(value)) {
|
|
163
|
+
value = parseInt(value);
|
|
164
|
+
} else if (/^\d+\.\d+$/.test(value)) {
|
|
165
|
+
value = parseFloat(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
values[keyWithoutPrefix] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return values as T;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// export given fields to all components
|
|
176
|
+
public exportContextFields(...fields: Array<keyof RequestContextData>): void {
|
|
177
|
+
fields.forEach((field) => {
|
|
178
|
+
if (! this.exportedRequestContextData.includes(field)) {
|
|
179
|
+
this.exportedRequestContextData.push(field);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// given file extension (or file name), returns the appropriate content-type
|
|
185
|
+
public contentType(extension: string): string|false {
|
|
186
|
+
return mime.contentType(extension);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// renders a component with give data and sends it as a response
|
|
190
|
+
private async respondWithComponent(ctx: RequestContext, componentName: string, attributes: RequestBodyArguments, unwrap: boolean = true): Promise<boolean> {
|
|
191
|
+
const component = this.components.getByName(componentName);
|
|
192
|
+
if (component) {
|
|
193
|
+
const document = new Document(this, '', ctx);
|
|
194
|
+
const data: LooseObject = attributes;
|
|
195
|
+
await document.loadComponent(component.name, data);
|
|
196
|
+
|
|
197
|
+
const exportedData = component.exportData ? document.data : (component.exportFields ? component.exportFields.reduce((prev, curr) => {
|
|
198
|
+
prev[curr] = document.children[0].data[curr];
|
|
199
|
+
return prev;
|
|
200
|
+
}, {} as LooseObject) : {});
|
|
201
|
+
|
|
202
|
+
ctx.respondWith({
|
|
203
|
+
html: document.children[0].dom[unwrap ? 'innerHTML' : 'outerHTML'],
|
|
204
|
+
initializers: document.initInitializers(),
|
|
205
|
+
data: exportedData
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public memoryUsage(): NodeJS.MemoryUsage {
|
|
214
|
+
return process.memoryUsage();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public printMemoryUsage(): void {
|
|
218
|
+
const usage = this.memoryUsage();
|
|
219
|
+
let total = 0;
|
|
220
|
+
const totals = Object.keys(usage).reduce((prev, key: keyof NodeJS.MemoryUsage) => {
|
|
221
|
+
const usedMb = usage[key] / 1000000;
|
|
222
|
+
prev[toSnakeCase(key).replaceAll('_', ' ')] = [parseFloat(usedMb.toFixed(1)), 'Mb'];
|
|
223
|
+
total += usedMb;
|
|
224
|
+
return prev;
|
|
225
|
+
}, {} as LooseObject);
|
|
226
|
+
totals.total = [parseFloat(total.toFixed(1)), 'Mb'];
|
|
227
|
+
console.table(totals);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
}
|