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,404 @@
|
|
|
1
|
+
import { Document } from './Document.js';
|
|
2
|
+
import { attributeValueFromString, attributeValueToString, objectEach, toCamelCase } from '../Util.js';
|
|
3
|
+
import { ComponentEntry, LooseObject } from '../Types.js';
|
|
4
|
+
|
|
5
|
+
import { RequestContextData } from '../../app/Types.js';
|
|
6
|
+
import { DOMFragment } from './dom/DOMFragment.js';
|
|
7
|
+
import { DOMNode } from './dom/DOMNode.js';
|
|
8
|
+
|
|
9
|
+
export class Component {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
document: Document;
|
|
13
|
+
|
|
14
|
+
parent: null|Document|Component;
|
|
15
|
+
children: Array<Component> = [];
|
|
16
|
+
|
|
17
|
+
path: Array<string> = [];
|
|
18
|
+
|
|
19
|
+
// all attributes found on component's tag
|
|
20
|
+
attributesRaw: Record<string, string | true> = {};
|
|
21
|
+
|
|
22
|
+
// extracted from data-attribute on component tag
|
|
23
|
+
attributes: Record<string, string|number|boolean|LooseObject|null> = {};
|
|
24
|
+
|
|
25
|
+
dom: DOMNode; // jsdom
|
|
26
|
+
|
|
27
|
+
data: LooseObject = {};
|
|
28
|
+
|
|
29
|
+
entry: null|ComponentEntry; // null for root
|
|
30
|
+
|
|
31
|
+
isRoot: boolean;
|
|
32
|
+
|
|
33
|
+
constructor(name: string, node?: DOMNode, parent?: Document|Component, autoInit: boolean = true) {
|
|
34
|
+
this.name = name;
|
|
35
|
+
|
|
36
|
+
if (name === 'root') {
|
|
37
|
+
this.dom = new DOMFragment();
|
|
38
|
+
this.path.push('');
|
|
39
|
+
this.isRoot = true;
|
|
40
|
+
} else {
|
|
41
|
+
this.dom = node || new DOMFragment();
|
|
42
|
+
if (parent) {
|
|
43
|
+
this.path = parent.path.concat(this.name);
|
|
44
|
+
}
|
|
45
|
+
this.isRoot = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this instanceof Document) {
|
|
49
|
+
// this will only happen if an instance of Document, as it extends component
|
|
50
|
+
this.document = this;
|
|
51
|
+
} else {
|
|
52
|
+
// component is always initialized with parent except when it is a document
|
|
53
|
+
// since here we know we are not a part of Document, parent has to be a component
|
|
54
|
+
if (! (parent instanceof Component)) {
|
|
55
|
+
console.error('Component initialized without a parent');
|
|
56
|
+
}
|
|
57
|
+
this.document = (parent as Component).document;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.parent = parent || null;
|
|
61
|
+
|
|
62
|
+
this.id = '';
|
|
63
|
+
|
|
64
|
+
const component = parent === undefined ? false : this.document.application.components.getByName(this.name);
|
|
65
|
+
if (component) {
|
|
66
|
+
// store ComponentEntry
|
|
67
|
+
this.entry = component;
|
|
68
|
+
|
|
69
|
+
if (autoInit) {
|
|
70
|
+
// fill in with HTML and init children
|
|
71
|
+
this.init(component.html);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
this.entry = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// load component's data and fill it
|
|
79
|
+
// load any nested components recursively
|
|
80
|
+
public async init(html: string, data?: LooseObject): Promise<void> {
|
|
81
|
+
|
|
82
|
+
// extract data-atributes and encode non-encoded attributes
|
|
83
|
+
this.initAttributesData();
|
|
84
|
+
|
|
85
|
+
// create component container replacng the original tag name with a div
|
|
86
|
+
// (or whatever is set as renderTagName on ComponentEntry)
|
|
87
|
+
this.dom.tagName = this.entry?.renderTagName || 'div';
|
|
88
|
+
|
|
89
|
+
// fill container with given HTML
|
|
90
|
+
this.dom.innerHTML = html;
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// re-apply attributes the orignal tag had
|
|
94
|
+
// no need to encode values at this point
|
|
95
|
+
// any non-encoded attributes got encoded earlier by initAttributesData
|
|
96
|
+
this.setAttributes(this.attributesRaw, '', false);
|
|
97
|
+
|
|
98
|
+
// store initializer function on owner Document
|
|
99
|
+
if (this.entry !== null && this.entry.initializer !== undefined && typeof this.document.initializers[this.name] === 'undefined') {
|
|
100
|
+
this.document.initializers[this.name] = this.entry.initializer;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// set data-structured-component="this.name" attribute on tag
|
|
104
|
+
this.dom.setAttribute(`data-${this.document.application.config.components.componentNameAttribute}`, this.name);
|
|
105
|
+
|
|
106
|
+
// allocate an unique ID for this component
|
|
107
|
+
// used client side to uniquely identify the component when it accesses it's storage
|
|
108
|
+
if (typeof this.attributes.componentId !== 'string') {
|
|
109
|
+
this.id = this.document.allocateId(this);
|
|
110
|
+
this.dom.setAttribute('data-component-id', this.id);
|
|
111
|
+
} else {
|
|
112
|
+
this.id = this.attributes.componentId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// export RequestContext.data fields specified in Application.exporteRequestContextData
|
|
116
|
+
const exportedContextData = this.document.application.exportedRequestContextData.reduce((prev, field) => {
|
|
117
|
+
if (! this.document.ctx) {return prev;}
|
|
118
|
+
if (field in this.document.ctx.data) {
|
|
119
|
+
prev[field] = this.document.ctx.data[field];
|
|
120
|
+
}
|
|
121
|
+
return prev;
|
|
122
|
+
}, {} as Record<keyof RequestContextData, any>)
|
|
123
|
+
objectEach(exportedContextData, (key, val) => {
|
|
124
|
+
if (this.document.application.exportedRequestContextData.includes(key)) {
|
|
125
|
+
this.setAttributes({ [key]: val }, 'data-', true);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.data = exportedContextData;
|
|
129
|
+
|
|
130
|
+
// if component is marked as deferred (module.deferred returns true), stop here
|
|
131
|
+
// ClientComponent will request a redraw as soon as it's initialized
|
|
132
|
+
// setting attributes.deferred = false, to avoid looping
|
|
133
|
+
if (
|
|
134
|
+
this.entry !== null &&
|
|
135
|
+
typeof this.entry.module !== 'undefined' &&
|
|
136
|
+
typeof this.entry.module.deferred === 'function' &&
|
|
137
|
+
this.entry.module.deferred(this.attributes, this.document.ctx, this.document.application) &&
|
|
138
|
+
this.attributes.deferred !== false
|
|
139
|
+
) {
|
|
140
|
+
this.setAttributes({deferred: true}, 'data-', true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof this.attributes.use === 'string' && this.parent !== null) {
|
|
145
|
+
// data-use was found on component tag
|
|
146
|
+
// if parent Component.data contains it, include it with data
|
|
147
|
+
// set data-component-parent when a component uses parent data
|
|
148
|
+
// it will be needed when the component is individually rendered
|
|
149
|
+
// componentInstances[j].setAttribute('data-component-parent', parentName);
|
|
150
|
+
this.attributes = Object.assign(this.importedParentData(this.parent.data) || {}, this.attributes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// load data
|
|
154
|
+
if (data === undefined) {
|
|
155
|
+
if (this.entry && this.entry.module) {
|
|
156
|
+
// component has a server side part, fetch data using getData
|
|
157
|
+
this.data = Object.assign(this.data, await this.entry.module.getData(this.attributes, this.document.ctx, this.document.application, this) || {});
|
|
158
|
+
} else {
|
|
159
|
+
// if the component has no server side part
|
|
160
|
+
// then use attributes as data
|
|
161
|
+
this.data = Object.assign(exportedContextData, this.attributes);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
this.data = Object.assign(exportedContextData, data, this.attributes);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// fill in before loading the components as user may output new components depending on the data
|
|
168
|
+
// eg. if data is an array user may output a ListItem component using Handlebars each
|
|
169
|
+
// we want those to be found as children
|
|
170
|
+
this.fillData(this.data);
|
|
171
|
+
|
|
172
|
+
if (this.entry === null || this.entry.exportData) {
|
|
173
|
+
// export all data if component has no server side part
|
|
174
|
+
this.setAttributes(this.data, 'data-');
|
|
175
|
+
} else if (this.entry) {
|
|
176
|
+
// export specified fields if it has a server side part
|
|
177
|
+
if (this.entry.exportFields) {
|
|
178
|
+
this.setAttributes(this.entry.exportFields.reduce((prev, field) => {
|
|
179
|
+
if (this.data[field] !== undefined) {
|
|
180
|
+
prev[field] = this.data[field];
|
|
181
|
+
}
|
|
182
|
+
return prev;
|
|
183
|
+
}, {} as Record<string, any>), 'data-');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// if attributes are present on ComponentEntry, add those to the DOM node
|
|
187
|
+
if (this.entry.attributes) {
|
|
188
|
+
this.setAttributes(this.entry.attributes, '', false);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await this.initChildren();
|
|
193
|
+
|
|
194
|
+
// add style display = none to all data-if's
|
|
195
|
+
// this will prevent twitching client side
|
|
196
|
+
// (otherwise elements that should be hidden might appear for a brief second)
|
|
197
|
+
if (this.isRoot) {
|
|
198
|
+
const dataIf = this.dom.queryByHasAttribute('data-if');
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < dataIf.length; i++) {
|
|
201
|
+
dataIf[i].style.display = 'none';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public setAttributes(attributes: Record<string, any>, prefix: string = '', encode: boolean = true): void {
|
|
207
|
+
if (typeof attributes === 'object' && attributes !== null) {
|
|
208
|
+
for (const attr in attributes) {
|
|
209
|
+
const encoded = typeof attributes[attr] === 'string' && attributes[attr].indexOf('base64:') === 0;
|
|
210
|
+
const value = (encode && !encoded) ? attributeValueToString(attr, attributes[attr]) : attributes[attr];
|
|
211
|
+
this.dom.setAttribute(prefix + attr, value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async initChildren(passData?: LooseObject): Promise<void> {
|
|
217
|
+
const componentTags = this.document.application.components.componentNames;
|
|
218
|
+
|
|
219
|
+
const childNodes = this.dom.queryByTagName(...componentTags);
|
|
220
|
+
// const promises: Array<Promise<void>> = [];
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
223
|
+
const childNode = childNodes[i];
|
|
224
|
+
const component = this.document.application.components.getByName(childNode.tagName);
|
|
225
|
+
if (component) {
|
|
226
|
+
const child = new Component(component.name, childNode, this, false);
|
|
227
|
+
// promises.push(child.init(childNode.outerHTML, passData));
|
|
228
|
+
await child.init(childNode.outerHTML, passData);
|
|
229
|
+
this.children.push(child);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// await Promise.all(promises);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// use string is coming from data-use attribute defined on the component
|
|
237
|
+
// use string can include multiple entries separated by a coma
|
|
238
|
+
// each entry can be a simple string which is the key in parent data
|
|
239
|
+
// but it can also use array item access key[index] and dot notation key.subkey or a combination key[index].subkey
|
|
240
|
+
protected importedParentData(parentData: LooseObject): LooseObject {
|
|
241
|
+
if (! this.parent) {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const data: LooseObject = {}
|
|
246
|
+
|
|
247
|
+
if (typeof this.attributes.use !== 'string') {
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// split by a coma and convert into array of "data paths"
|
|
252
|
+
// data path is an array of strings and numbers, and it's used to navigate the given parentData and extract a value
|
|
253
|
+
const usePaths: Array<Array<string|number>> = this.attributes.use.split(',').map((key) => {
|
|
254
|
+
return key.split(/\.|\[(\d+)\]/).filter((s) => {return s !== undefined && s.length > 0 }).map((s) => {
|
|
255
|
+
return /^\d+$/.test(s) ? parseInt(s) : s;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// try to extract data for each path
|
|
260
|
+
usePaths.forEach((dataPath) => {
|
|
261
|
+
let dataCurrent:any = parentData;
|
|
262
|
+
for (let i = 0; i < dataPath.length; i++) {
|
|
263
|
+
const segment = dataPath[i];
|
|
264
|
+
if (typeof dataCurrent[segment] === 'undefined') {
|
|
265
|
+
// not included in parentData, skip
|
|
266
|
+
dataCurrent = undefined;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
dataCurrent = dataCurrent[segment];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// last segment is the key
|
|
273
|
+
const dataKey = dataPath[dataPath.length - 1];
|
|
274
|
+
|
|
275
|
+
// set the data
|
|
276
|
+
data[dataKey] = dataCurrent;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (usePaths.length == 1 && typeof usePaths[0][usePaths[0].length - 1] === 'number') {
|
|
280
|
+
// if only a single import
|
|
281
|
+
// and it ends with a number (indexed array) do not return { number : data }
|
|
282
|
+
// instead return the data
|
|
283
|
+
return data[usePaths[0][usePaths[0].length - 1]];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return data;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// fill this.attributes and this.attributesRaw using attributes found on domNode
|
|
290
|
+
// encode all non-encoded attributes using attributeValueToString
|
|
291
|
+
protected initAttributesData(domNode?: DOMNode): void {
|
|
292
|
+
if (domNode === undefined) {
|
|
293
|
+
domNode = this.dom;
|
|
294
|
+
}
|
|
295
|
+
for (let i = 0; i < domNode.attributes.length; i++) {
|
|
296
|
+
const attrNameRaw = domNode.attributes[i].name;
|
|
297
|
+
|
|
298
|
+
// attributes can have a data prefix eg. number:data-num="3"
|
|
299
|
+
// return unprefixed attribute name
|
|
300
|
+
const attrNameUnprefixed = this.attributeUnpreffixed(attrNameRaw);
|
|
301
|
+
|
|
302
|
+
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
303
|
+
// only attributes starting with data- are stored to this.attributes
|
|
304
|
+
// rest are only kept in attributesRaw
|
|
305
|
+
const attrDataType = this.attributeDataType(attrNameRaw);
|
|
306
|
+
|
|
307
|
+
// attributes will usually be encoded using attributeValueToString, decode the value
|
|
308
|
+
// using attributeValueFromString, if it was encoded dataDecoded is { key: string, value: any }
|
|
309
|
+
// otherwise dataDecoded is a string
|
|
310
|
+
const dataDecoded = attributeValueFromString(domNode.attributes[i].value.toString());
|
|
311
|
+
|
|
312
|
+
// store the fact whether value was encoded, we need it later
|
|
313
|
+
const valueEncoded = typeof dataDecoded === 'object';
|
|
314
|
+
|
|
315
|
+
// value in it's raw form
|
|
316
|
+
// if the value was encoded it has correct type
|
|
317
|
+
// if the value was not encoded it may have incorrect type (solved later)
|
|
318
|
+
let value = valueEncoded ? dataDecoded.value as string|number|boolean|LooseObject|null : dataDecoded;
|
|
319
|
+
|
|
320
|
+
// key of encoded values is preserved as-is
|
|
321
|
+
// key of non-encoded values is in-dashed-form, if so, we convert it to camel case
|
|
322
|
+
const key = valueEncoded ? dataDecoded.key : toCamelCase(attrNameUnprefixed.substring(5));
|
|
323
|
+
|
|
324
|
+
if (! valueEncoded) {
|
|
325
|
+
// value was not encoded
|
|
326
|
+
if (typeof value === 'string') {
|
|
327
|
+
// data type of value is currently string as the value was not encoded
|
|
328
|
+
// data-attr may have had a data type prefix, if so, make sure data type is restored
|
|
329
|
+
if (attrDataType === 'number') {
|
|
330
|
+
value = parseFloat(value);
|
|
331
|
+
} else if (attrDataType === 'boolean') {
|
|
332
|
+
value = value === 'true' || value === '1';
|
|
333
|
+
} else if (attrDataType === 'object') {
|
|
334
|
+
if (typeof value === 'string') {
|
|
335
|
+
if (value.trim().length > 1) {
|
|
336
|
+
value = JSON.parse(value);
|
|
337
|
+
} else {
|
|
338
|
+
value = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// encode attribute value using attributeValueToString
|
|
345
|
+
const attrData = attributeValueToString(key, value);
|
|
346
|
+
domNode.setAttribute(attrNameRaw, attrData);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// store value
|
|
350
|
+
this.attributes[key] = value;
|
|
351
|
+
}
|
|
352
|
+
this.attributesRaw[attrNameRaw] = domNode.attributes[i].value;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// component attributes can have a data type prefix [prefix]:data-[name]="[val]"
|
|
357
|
+
// returns the prefix
|
|
358
|
+
private attributePreffix(attrName: string): string|null {
|
|
359
|
+
const index = attrName.indexOf(':');
|
|
360
|
+
if (index < 0) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return attrName.substring(0, index);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// returns the user defined data type of given attribute
|
|
367
|
+
// for example number:data-total returns 'number'
|
|
368
|
+
private attributeDataType(attrName: string): 'string'|'number'|'object'|'boolean'|'any' {
|
|
369
|
+
const prefix = this.attributePreffix(attrName);
|
|
370
|
+
|
|
371
|
+
if (
|
|
372
|
+
prefix === 'string' ||
|
|
373
|
+
prefix === 'number' ||
|
|
374
|
+
prefix === 'object' ||
|
|
375
|
+
prefix === 'boolean'
|
|
376
|
+
) {
|
|
377
|
+
return prefix;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// unrecognized attribute preffix
|
|
381
|
+
return 'any';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// removes the data-type prefix from given attribute name
|
|
385
|
+
// for example number:data-total returns data-total
|
|
386
|
+
private attributeUnpreffixed(attrName: string): string {
|
|
387
|
+
const index = attrName.indexOf(':');
|
|
388
|
+
if (index < 0) {
|
|
389
|
+
return attrName;
|
|
390
|
+
}
|
|
391
|
+
return attrName.substring(index + 1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// compile/fill in data for current component
|
|
395
|
+
protected fillData(data: LooseObject): void {
|
|
396
|
+
if (this.entry && this.entry.static === true) {
|
|
397
|
+
// defined as static component, skip compilation
|
|
398
|
+
this.dom.innerHTML = this.entry.html;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const html = this.entry ? this.entry.html : this.dom.innerHTML;
|
|
402
|
+
this.dom.innerHTML = this.document.application.handlebars.compile(html, data);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentEntry, StructuredConfig } from '../Types';
|
|
4
|
+
import { Application } from './Application.js';
|
|
5
|
+
|
|
6
|
+
export class Components {
|
|
7
|
+
|
|
8
|
+
config: StructuredConfig;
|
|
9
|
+
|
|
10
|
+
// upper-case component name -> ComponentEntry
|
|
11
|
+
private readonly components: Record<string, ComponentEntry> = {};
|
|
12
|
+
componentNames: Array<string> = [];
|
|
13
|
+
|
|
14
|
+
constructor(app: Application) {
|
|
15
|
+
this.config = app.config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public loadComponents(relativeToPath?: string): void {
|
|
19
|
+
if (relativeToPath === undefined) {
|
|
20
|
+
relativeToPath = path.resolve((this.config.runtime === 'Node.js' ? '../' : './') + this.config.components.path);
|
|
21
|
+
}
|
|
22
|
+
const components = readdirSync(relativeToPath);
|
|
23
|
+
|
|
24
|
+
components.forEach(async (component) => {
|
|
25
|
+
// check if directory
|
|
26
|
+
// absolute path to a directory, or the component's HTML file
|
|
27
|
+
const absolutePath = relativeToPath + '/' + component;
|
|
28
|
+
const isDirectory = statSync(absolutePath).isDirectory();
|
|
29
|
+
|
|
30
|
+
if (isDirectory) {
|
|
31
|
+
this.loadComponents(absolutePath);
|
|
32
|
+
} else {
|
|
33
|
+
// file, register component entry
|
|
34
|
+
if (component.endsWith('.html') || component.endsWith('.hbs')) {
|
|
35
|
+
// remove .html to get componentName
|
|
36
|
+
const componentNameParts = component.split('.');
|
|
37
|
+
const componentName = componentNameParts.slice(0, componentNameParts.length - 1).join('.');
|
|
38
|
+
|
|
39
|
+
const pathAbsolute = relativeToPath || '';
|
|
40
|
+
const pathRelative = path.relative(this.config.runtime === 'Node.js' ? '../' : './', pathAbsolute);
|
|
41
|
+
const pathBuild = path.resolve('./' + pathRelative);
|
|
42
|
+
const pathRelativeToViews = path.relative(`./${this.config.components.path}`, pathRelative);
|
|
43
|
+
|
|
44
|
+
const pathHTML = `${pathAbsolute}/${component}`;
|
|
45
|
+
|
|
46
|
+
// server side js file path (may not exist)
|
|
47
|
+
const jsServerPath = `${pathBuild}/${componentName}.${this.config.runtime === 'Node.js' ? 'js' : 'ts'}`;
|
|
48
|
+
const hasServerJS = existsSync(jsServerPath);
|
|
49
|
+
|
|
50
|
+
// client side js file path (may not exist)
|
|
51
|
+
const jsClientPath = `${pathBuild}/${componentName}.client.${this.config.runtime === 'Node.js' ? 'js' : 'ts'}`;
|
|
52
|
+
const hasClientJS = existsSync(jsClientPath);
|
|
53
|
+
|
|
54
|
+
const entry: ComponentEntry = {
|
|
55
|
+
name: componentName,
|
|
56
|
+
path: {
|
|
57
|
+
absolute: pathAbsolute,
|
|
58
|
+
relative: pathRelative,
|
|
59
|
+
relativeToViews: `${pathRelativeToViews}/${component}`,
|
|
60
|
+
build: pathBuild,
|
|
61
|
+
html: pathHTML,
|
|
62
|
+
jsClient: hasClientJS ? jsClientPath : undefined,
|
|
63
|
+
jsServer: hasServerJS ? jsServerPath : undefined
|
|
64
|
+
},
|
|
65
|
+
hasJS : existsSync(jsServerPath),
|
|
66
|
+
html: this.loadHTML(absolutePath),
|
|
67
|
+
exportData: false,
|
|
68
|
+
static: false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// load client side initializer
|
|
72
|
+
if (hasClientJS) {
|
|
73
|
+
const initializer = await import('file:///' + jsClientPath);
|
|
74
|
+
entry.initializer = initializer.init;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (hasServerJS) {
|
|
78
|
+
// load and instantiate component's module
|
|
79
|
+
const componentConstructor = await import('file:///' + entry.path.jsServer);
|
|
80
|
+
entry.module = new componentConstructor.default();
|
|
81
|
+
|
|
82
|
+
entry.renderTagName = entry.module?.tagName || 'div';
|
|
83
|
+
entry.exportData = typeof entry.module?.exportData === 'boolean' ? entry.module.exportData : false;
|
|
84
|
+
entry.exportFields = entry.module?.exportFields;
|
|
85
|
+
entry.attributes = entry.module?.attributes;
|
|
86
|
+
entry.static = typeof entry.module?.static === 'boolean' ? entry.module.static : false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.components[componentName.toUpperCase()] = entry;
|
|
90
|
+
this.componentNames.push(entry.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// get component by name
|
|
97
|
+
public getByName(name: string): null|ComponentEntry {
|
|
98
|
+
return this.components[name.toUpperCase()] || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// load HTML from given path
|
|
102
|
+
private loadHTML(path: string): string {
|
|
103
|
+
return this.stripComments(readFileSync(path).toString());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// remove all HTML comments
|
|
107
|
+
private stripComments(html: string): string {
|
|
108
|
+
return html.replaceAll(/<!--(?!-?>)(?!.*--!>)(?!.*<!--(?!>)).*?(?<!<!-)-->/g, '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { LooseObject } from "../Types.js";
|
|
3
|
+
|
|
4
|
+
export class Cookies {
|
|
5
|
+
|
|
6
|
+
// parse cookies sent with given request into an object
|
|
7
|
+
public parse(request: IncomingMessage): LooseObject {
|
|
8
|
+
if (! request.headers.cookie) {return {};}
|
|
9
|
+
const cookieString = request.headers.cookie;
|
|
10
|
+
const cookiePairs = cookieString.split(';');
|
|
11
|
+
|
|
12
|
+
const cookies: LooseObject = {}
|
|
13
|
+
|
|
14
|
+
cookiePairs.forEach((cookiePair) => {
|
|
15
|
+
const parts = cookiePair.trim().split('=');
|
|
16
|
+
cookies[parts.shift() || ''] = parts.join('=');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return cookies;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// set a cookie for given response
|
|
23
|
+
// sets the Set-Cookie header, which will be sent with the output
|
|
24
|
+
public set(response: ServerResponse, name: string, value: string|number, lifetimeSeconds: number, path: string = '/', sameSite: 'Strict' | 'Lax' | 'None' = 'Strict', domain?: string): void {
|
|
25
|
+
const expiresAt = lifetimeSeconds > 0 ? new Date(new Date().getTime() + lifetimeSeconds * 1000).toUTCString() : 0;
|
|
26
|
+
response.appendHeader('Set-Cookie', `${name}=${value}; Expires=${expiresAt}; Path=${path}; SameSite=${sameSite}${domain ? `; domain=${domain}` : ''}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|