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,249 @@
|
|
|
1
|
+
import { Document } from './Document.js';
|
|
2
|
+
import { attributeValueFromString, attributeValueToString, objectEach, toCamelCase } from '../Util.js';
|
|
3
|
+
import { DOMFragment } from './dom/DOMFragment.js';
|
|
4
|
+
export class Component {
|
|
5
|
+
constructor(name, node, parent, autoInit = true) {
|
|
6
|
+
this.children = [];
|
|
7
|
+
this.path = [];
|
|
8
|
+
this.attributesRaw = {};
|
|
9
|
+
this.attributes = {};
|
|
10
|
+
this.data = {};
|
|
11
|
+
this.name = name;
|
|
12
|
+
if (name === 'root') {
|
|
13
|
+
this.dom = new DOMFragment();
|
|
14
|
+
this.path.push('');
|
|
15
|
+
this.isRoot = true;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.dom = node || new DOMFragment();
|
|
19
|
+
if (parent) {
|
|
20
|
+
this.path = parent.path.concat(this.name);
|
|
21
|
+
}
|
|
22
|
+
this.isRoot = false;
|
|
23
|
+
}
|
|
24
|
+
if (this instanceof Document) {
|
|
25
|
+
this.document = this;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (!(parent instanceof Component)) {
|
|
29
|
+
console.error('Component initialized without a parent');
|
|
30
|
+
}
|
|
31
|
+
this.document = parent.document;
|
|
32
|
+
}
|
|
33
|
+
this.parent = parent || null;
|
|
34
|
+
this.id = '';
|
|
35
|
+
const component = parent === undefined ? false : this.document.application.components.getByName(this.name);
|
|
36
|
+
if (component) {
|
|
37
|
+
this.entry = component;
|
|
38
|
+
if (autoInit) {
|
|
39
|
+
this.init(component.html);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.entry = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async init(html, data) {
|
|
47
|
+
this.initAttributesData();
|
|
48
|
+
this.dom.tagName = this.entry?.renderTagName || 'div';
|
|
49
|
+
this.dom.innerHTML = html;
|
|
50
|
+
this.setAttributes(this.attributesRaw, '', false);
|
|
51
|
+
if (this.entry !== null && this.entry.initializer !== undefined && typeof this.document.initializers[this.name] === 'undefined') {
|
|
52
|
+
this.document.initializers[this.name] = this.entry.initializer;
|
|
53
|
+
}
|
|
54
|
+
this.dom.setAttribute(`data-${this.document.application.config.components.componentNameAttribute}`, this.name);
|
|
55
|
+
if (typeof this.attributes.componentId !== 'string') {
|
|
56
|
+
this.id = this.document.allocateId(this);
|
|
57
|
+
this.dom.setAttribute('data-component-id', this.id);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.id = this.attributes.componentId;
|
|
61
|
+
}
|
|
62
|
+
const exportedContextData = this.document.application.exportedRequestContextData.reduce((prev, field) => {
|
|
63
|
+
if (!this.document.ctx) {
|
|
64
|
+
return prev;
|
|
65
|
+
}
|
|
66
|
+
if (field in this.document.ctx.data) {
|
|
67
|
+
prev[field] = this.document.ctx.data[field];
|
|
68
|
+
}
|
|
69
|
+
return prev;
|
|
70
|
+
}, {});
|
|
71
|
+
objectEach(exportedContextData, (key, val) => {
|
|
72
|
+
if (this.document.application.exportedRequestContextData.includes(key)) {
|
|
73
|
+
this.setAttributes({ [key]: val }, 'data-', true);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.data = exportedContextData;
|
|
77
|
+
if (this.entry !== null &&
|
|
78
|
+
typeof this.entry.module !== 'undefined' &&
|
|
79
|
+
typeof this.entry.module.deferred === 'function' &&
|
|
80
|
+
this.entry.module.deferred(this.attributes, this.document.ctx, this.document.application) &&
|
|
81
|
+
this.attributes.deferred !== false) {
|
|
82
|
+
this.setAttributes({ deferred: true }, 'data-', true);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (typeof this.attributes.use === 'string' && this.parent !== null) {
|
|
86
|
+
this.attributes = Object.assign(this.importedParentData(this.parent.data) || {}, this.attributes);
|
|
87
|
+
}
|
|
88
|
+
if (data === undefined) {
|
|
89
|
+
if (this.entry && this.entry.module) {
|
|
90
|
+
this.data = Object.assign(this.data, await this.entry.module.getData(this.attributes, this.document.ctx, this.document.application, this) || {});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.data = Object.assign(exportedContextData, this.attributes);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.data = Object.assign(exportedContextData, data, this.attributes);
|
|
98
|
+
}
|
|
99
|
+
this.fillData(this.data);
|
|
100
|
+
if (this.entry === null || this.entry.exportData) {
|
|
101
|
+
this.setAttributes(this.data, 'data-');
|
|
102
|
+
}
|
|
103
|
+
else if (this.entry) {
|
|
104
|
+
if (this.entry.exportFields) {
|
|
105
|
+
this.setAttributes(this.entry.exportFields.reduce((prev, field) => {
|
|
106
|
+
if (this.data[field] !== undefined) {
|
|
107
|
+
prev[field] = this.data[field];
|
|
108
|
+
}
|
|
109
|
+
return prev;
|
|
110
|
+
}, {}), 'data-');
|
|
111
|
+
}
|
|
112
|
+
if (this.entry.attributes) {
|
|
113
|
+
this.setAttributes(this.entry.attributes, '', false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await this.initChildren();
|
|
117
|
+
if (this.isRoot) {
|
|
118
|
+
const dataIf = this.dom.queryByHasAttribute('data-if');
|
|
119
|
+
for (let i = 0; i < dataIf.length; i++) {
|
|
120
|
+
dataIf[i].style.display = 'none';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
setAttributes(attributes, prefix = '', encode = true) {
|
|
125
|
+
if (typeof attributes === 'object' && attributes !== null) {
|
|
126
|
+
for (const attr in attributes) {
|
|
127
|
+
const encoded = typeof attributes[attr] === 'string' && attributes[attr].indexOf('base64:') === 0;
|
|
128
|
+
const value = (encode && !encoded) ? attributeValueToString(attr, attributes[attr]) : attributes[attr];
|
|
129
|
+
this.dom.setAttribute(prefix + attr, value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async initChildren(passData) {
|
|
134
|
+
const componentTags = this.document.application.components.componentNames;
|
|
135
|
+
const childNodes = this.dom.queryByTagName(...componentTags);
|
|
136
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
137
|
+
const childNode = childNodes[i];
|
|
138
|
+
const component = this.document.application.components.getByName(childNode.tagName);
|
|
139
|
+
if (component) {
|
|
140
|
+
const child = new Component(component.name, childNode, this, false);
|
|
141
|
+
await child.init(childNode.outerHTML, passData);
|
|
142
|
+
this.children.push(child);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
importedParentData(parentData) {
|
|
147
|
+
if (!this.parent) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
const data = {};
|
|
151
|
+
if (typeof this.attributes.use !== 'string') {
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
const usePaths = this.attributes.use.split(',').map((key) => {
|
|
155
|
+
return key.split(/\.|\[(\d+)\]/).filter((s) => { return s !== undefined && s.length > 0; }).map((s) => {
|
|
156
|
+
return /^\d+$/.test(s) ? parseInt(s) : s;
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
usePaths.forEach((dataPath) => {
|
|
160
|
+
let dataCurrent = parentData;
|
|
161
|
+
for (let i = 0; i < dataPath.length; i++) {
|
|
162
|
+
const segment = dataPath[i];
|
|
163
|
+
if (typeof dataCurrent[segment] === 'undefined') {
|
|
164
|
+
dataCurrent = undefined;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
dataCurrent = dataCurrent[segment];
|
|
168
|
+
}
|
|
169
|
+
const dataKey = dataPath[dataPath.length - 1];
|
|
170
|
+
data[dataKey] = dataCurrent;
|
|
171
|
+
});
|
|
172
|
+
if (usePaths.length == 1 && typeof usePaths[0][usePaths[0].length - 1] === 'number') {
|
|
173
|
+
return data[usePaths[0][usePaths[0].length - 1]];
|
|
174
|
+
}
|
|
175
|
+
return data;
|
|
176
|
+
}
|
|
177
|
+
initAttributesData(domNode) {
|
|
178
|
+
if (domNode === undefined) {
|
|
179
|
+
domNode = this.dom;
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < domNode.attributes.length; i++) {
|
|
182
|
+
const attrNameRaw = domNode.attributes[i].name;
|
|
183
|
+
const attrNameUnprefixed = this.attributeUnpreffixed(attrNameRaw);
|
|
184
|
+
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
185
|
+
const attrDataType = this.attributeDataType(attrNameRaw);
|
|
186
|
+
const dataDecoded = attributeValueFromString(domNode.attributes[i].value.toString());
|
|
187
|
+
const valueEncoded = typeof dataDecoded === 'object';
|
|
188
|
+
let value = valueEncoded ? dataDecoded.value : dataDecoded;
|
|
189
|
+
const key = valueEncoded ? dataDecoded.key : toCamelCase(attrNameUnprefixed.substring(5));
|
|
190
|
+
if (!valueEncoded) {
|
|
191
|
+
if (typeof value === 'string') {
|
|
192
|
+
if (attrDataType === 'number') {
|
|
193
|
+
value = parseFloat(value);
|
|
194
|
+
}
|
|
195
|
+
else if (attrDataType === 'boolean') {
|
|
196
|
+
value = value === 'true' || value === '1';
|
|
197
|
+
}
|
|
198
|
+
else if (attrDataType === 'object') {
|
|
199
|
+
if (typeof value === 'string') {
|
|
200
|
+
if (value.trim().length > 1) {
|
|
201
|
+
value = JSON.parse(value);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
value = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const attrData = attributeValueToString(key, value);
|
|
210
|
+
domNode.setAttribute(attrNameRaw, attrData);
|
|
211
|
+
}
|
|
212
|
+
this.attributes[key] = value;
|
|
213
|
+
}
|
|
214
|
+
this.attributesRaw[attrNameRaw] = domNode.attributes[i].value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
attributePreffix(attrName) {
|
|
218
|
+
const index = attrName.indexOf(':');
|
|
219
|
+
if (index < 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return attrName.substring(0, index);
|
|
223
|
+
}
|
|
224
|
+
attributeDataType(attrName) {
|
|
225
|
+
const prefix = this.attributePreffix(attrName);
|
|
226
|
+
if (prefix === 'string' ||
|
|
227
|
+
prefix === 'number' ||
|
|
228
|
+
prefix === 'object' ||
|
|
229
|
+
prefix === 'boolean') {
|
|
230
|
+
return prefix;
|
|
231
|
+
}
|
|
232
|
+
return 'any';
|
|
233
|
+
}
|
|
234
|
+
attributeUnpreffixed(attrName) {
|
|
235
|
+
const index = attrName.indexOf(':');
|
|
236
|
+
if (index < 0) {
|
|
237
|
+
return attrName;
|
|
238
|
+
}
|
|
239
|
+
return attrName.substring(index + 1);
|
|
240
|
+
}
|
|
241
|
+
fillData(data) {
|
|
242
|
+
if (this.entry && this.entry.static === true) {
|
|
243
|
+
this.dom.innerHTML = this.entry.html;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const html = this.entry ? this.entry.html : this.dom.innerHTML;
|
|
247
|
+
this.dom.innerHTML = this.document.application.handlebars.compile(html, data);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ComponentEntry, StructuredConfig } from '../Types';
|
|
2
|
+
import { Application } from './Application.js';
|
|
3
|
+
export declare class Components {
|
|
4
|
+
config: StructuredConfig;
|
|
5
|
+
private readonly components;
|
|
6
|
+
componentNames: Array<string>;
|
|
7
|
+
constructor(app: Application);
|
|
8
|
+
loadComponents(relativeToPath?: string): void;
|
|
9
|
+
getByName(name: string): null | ComponentEntry;
|
|
10
|
+
private loadHTML;
|
|
11
|
+
private stripComments;
|
|
12
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export class Components {
|
|
4
|
+
constructor(app) {
|
|
5
|
+
this.components = {};
|
|
6
|
+
this.componentNames = [];
|
|
7
|
+
this.config = app.config;
|
|
8
|
+
}
|
|
9
|
+
loadComponents(relativeToPath) {
|
|
10
|
+
if (relativeToPath === undefined) {
|
|
11
|
+
relativeToPath = path.resolve((this.config.runtime === 'Node.js' ? '../' : './') + this.config.components.path);
|
|
12
|
+
}
|
|
13
|
+
const components = readdirSync(relativeToPath);
|
|
14
|
+
components.forEach(async (component) => {
|
|
15
|
+
const absolutePath = relativeToPath + '/' + component;
|
|
16
|
+
const isDirectory = statSync(absolutePath).isDirectory();
|
|
17
|
+
if (isDirectory) {
|
|
18
|
+
this.loadComponents(absolutePath);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (component.endsWith('.html') || component.endsWith('.hbs')) {
|
|
22
|
+
const componentNameParts = component.split('.');
|
|
23
|
+
const componentName = componentNameParts.slice(0, componentNameParts.length - 1).join('.');
|
|
24
|
+
const pathAbsolute = relativeToPath || '';
|
|
25
|
+
const pathRelative = path.relative(this.config.runtime === 'Node.js' ? '../' : './', pathAbsolute);
|
|
26
|
+
const pathBuild = path.resolve('./' + pathRelative);
|
|
27
|
+
const pathRelativeToViews = path.relative(`./${this.config.components.path}`, pathRelative);
|
|
28
|
+
const pathHTML = `${pathAbsolute}/${component}`;
|
|
29
|
+
const jsServerPath = `${pathBuild}/${componentName}.${this.config.runtime === 'Node.js' ? 'js' : 'ts'}`;
|
|
30
|
+
const hasServerJS = existsSync(jsServerPath);
|
|
31
|
+
const jsClientPath = `${pathBuild}/${componentName}.client.${this.config.runtime === 'Node.js' ? 'js' : 'ts'}`;
|
|
32
|
+
const hasClientJS = existsSync(jsClientPath);
|
|
33
|
+
const entry = {
|
|
34
|
+
name: componentName,
|
|
35
|
+
path: {
|
|
36
|
+
absolute: pathAbsolute,
|
|
37
|
+
relative: pathRelative,
|
|
38
|
+
relativeToViews: `${pathRelativeToViews}/${component}`,
|
|
39
|
+
build: pathBuild,
|
|
40
|
+
html: pathHTML,
|
|
41
|
+
jsClient: hasClientJS ? jsClientPath : undefined,
|
|
42
|
+
jsServer: hasServerJS ? jsServerPath : undefined
|
|
43
|
+
},
|
|
44
|
+
hasJS: existsSync(jsServerPath),
|
|
45
|
+
html: this.loadHTML(absolutePath),
|
|
46
|
+
exportData: false,
|
|
47
|
+
static: false
|
|
48
|
+
};
|
|
49
|
+
if (hasClientJS) {
|
|
50
|
+
const initializer = await import('file:///' + jsClientPath);
|
|
51
|
+
entry.initializer = initializer.init;
|
|
52
|
+
}
|
|
53
|
+
if (hasServerJS) {
|
|
54
|
+
const componentConstructor = await import('file:///' + entry.path.jsServer);
|
|
55
|
+
entry.module = new componentConstructor.default();
|
|
56
|
+
entry.renderTagName = entry.module?.tagName || 'div';
|
|
57
|
+
entry.exportData = typeof entry.module?.exportData === 'boolean' ? entry.module.exportData : false;
|
|
58
|
+
entry.exportFields = entry.module?.exportFields;
|
|
59
|
+
entry.attributes = entry.module?.attributes;
|
|
60
|
+
entry.static = typeof entry.module?.static === 'boolean' ? entry.module.static : false;
|
|
61
|
+
}
|
|
62
|
+
this.components[componentName.toUpperCase()] = entry;
|
|
63
|
+
this.componentNames.push(entry.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
getByName(name) {
|
|
69
|
+
return this.components[name.toUpperCase()] || null;
|
|
70
|
+
}
|
|
71
|
+
loadHTML(path) {
|
|
72
|
+
return this.stripComments(readFileSync(path).toString());
|
|
73
|
+
}
|
|
74
|
+
stripComments(html) {
|
|
75
|
+
return html.replaceAll(/<!--(?!-?>)(?!.*--!>)(?!.*<!--(?!>)).*?(?<!<!-)-->/g, '');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { LooseObject } from "../Types.js";
|
|
3
|
+
export declare class Cookies {
|
|
4
|
+
parse(request: IncomingMessage): LooseObject;
|
|
5
|
+
set(response: ServerResponse, name: string, value: string | number, lifetimeSeconds: number, path?: string, sameSite?: 'Strict' | 'Lax' | 'None', domain?: string): void;
|
|
6
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class Cookies {
|
|
2
|
+
parse(request) {
|
|
3
|
+
if (!request.headers.cookie) {
|
|
4
|
+
return {};
|
|
5
|
+
}
|
|
6
|
+
const cookieString = request.headers.cookie;
|
|
7
|
+
const cookiePairs = cookieString.split(';');
|
|
8
|
+
const cookies = {};
|
|
9
|
+
cookiePairs.forEach((cookiePair) => {
|
|
10
|
+
const parts = cookiePair.trim().split('=');
|
|
11
|
+
cookies[parts.shift() || ''] = parts.join('=');
|
|
12
|
+
});
|
|
13
|
+
return cookies;
|
|
14
|
+
}
|
|
15
|
+
set(response, name, value, lifetimeSeconds, path = '/', sameSite = 'Strict', domain) {
|
|
16
|
+
const expiresAt = lifetimeSeconds > 0 ? new Date(new Date().getTime() + lifetimeSeconds * 1000).toUTCString() : 0;
|
|
17
|
+
response.appendHeader('Set-Cookie', `${name}=${value}; Expires=${expiresAt}; Path=${path}; SameSite=${sameSite}${domain ? `; domain=${domain}` : ''}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ServerResponse } from 'node:http';
|
|
2
|
+
import { Initializers, LooseObject, RequestContext } from '../../system/Types.js';
|
|
3
|
+
import { Application } from './Application.js';
|
|
4
|
+
import { DocumentHead } from './DocumentHead.js';
|
|
5
|
+
import { Component } from './Component.js';
|
|
6
|
+
export declare class Document extends Component {
|
|
7
|
+
head: DocumentHead;
|
|
8
|
+
language: string;
|
|
9
|
+
application: Application;
|
|
10
|
+
initializers: Initializers;
|
|
11
|
+
initializersInitialized: boolean;
|
|
12
|
+
componentIds: Array<string>;
|
|
13
|
+
ctx: undefined | RequestContext;
|
|
14
|
+
appendHTML: string;
|
|
15
|
+
constructor(app: Application, title: string, ctx?: RequestContext);
|
|
16
|
+
push(response: ServerResponse): void;
|
|
17
|
+
body(): string;
|
|
18
|
+
initInitializers(): Record<string, string>;
|
|
19
|
+
private initClientConfig;
|
|
20
|
+
toString(): string;
|
|
21
|
+
allocateId(component: Component): string;
|
|
22
|
+
loadView(pathRelative: string, data?: LooseObject): Promise<boolean>;
|
|
23
|
+
loadComponent(componentName: string, data?: LooseObject): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Md5 } from 'ts-md5';
|
|
2
|
+
import { DocumentHead } from './DocumentHead.js';
|
|
3
|
+
import { Component } from './Component.js';
|
|
4
|
+
import { attributeValueToString, randomString } from '../Util.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
export class Document extends Component {
|
|
8
|
+
constructor(app, title, ctx) {
|
|
9
|
+
super('root');
|
|
10
|
+
this.language = 'en';
|
|
11
|
+
this.initializers = {};
|
|
12
|
+
this.initializersInitialized = false;
|
|
13
|
+
this.componentIds = [];
|
|
14
|
+
this.appendHTML = '';
|
|
15
|
+
this.application = app;
|
|
16
|
+
this.ctx = ctx;
|
|
17
|
+
this.document = this;
|
|
18
|
+
this.head = new DocumentHead(title);
|
|
19
|
+
this.head.addJS('/assets/client-js/client/Client.js', 0, { type: 'module' });
|
|
20
|
+
this.application.emit('documentCreated', this);
|
|
21
|
+
}
|
|
22
|
+
push(response) {
|
|
23
|
+
const resourcesJS = this.head.js.map((resource) => {
|
|
24
|
+
return `<${resource.path}>; rel=${this.application.config.http.linkHeaderRel}; as=script; crossorigin=anonymous`;
|
|
25
|
+
});
|
|
26
|
+
const resourcesCSS = this.head.css.map((resource) => {
|
|
27
|
+
return `<${resource.path}>; rel=${this.application.config.http.linkHeaderRel}; as=style; crossorigin=anonymous`;
|
|
28
|
+
});
|
|
29
|
+
const value = resourcesCSS.concat(resourcesJS).join(', ');
|
|
30
|
+
response.setHeader('Link', value);
|
|
31
|
+
}
|
|
32
|
+
body() {
|
|
33
|
+
return this.dom.innerHTML + '\n' + this.appendHTML;
|
|
34
|
+
}
|
|
35
|
+
initInitializers() {
|
|
36
|
+
const initializers = {};
|
|
37
|
+
for (const name in this.initializers) {
|
|
38
|
+
initializers[name] = this.initializers[name].toString();
|
|
39
|
+
}
|
|
40
|
+
const initializersString = '<script type="application/javascript">window.initializers = ' + JSON.stringify(initializers) + '</script>';
|
|
41
|
+
this.head.add(initializersString);
|
|
42
|
+
this.initializersInitialized = true;
|
|
43
|
+
return initializers;
|
|
44
|
+
}
|
|
45
|
+
initClientConfig() {
|
|
46
|
+
const clientConf = {
|
|
47
|
+
componentRender: this.application.config.url.componentRender,
|
|
48
|
+
componentNameAttribute: this.application.config.components.componentNameAttribute
|
|
49
|
+
};
|
|
50
|
+
const clientConfString = `<script type="application/javascript">window.structuredClientConfig = ${JSON.stringify(clientConf)}</script>`;
|
|
51
|
+
this.head.add(clientConfString);
|
|
52
|
+
}
|
|
53
|
+
toString() {
|
|
54
|
+
if (!this.initializersInitialized) {
|
|
55
|
+
this.initInitializers();
|
|
56
|
+
this.initClientConfig();
|
|
57
|
+
}
|
|
58
|
+
return `<!DOCTYPE html>
|
|
59
|
+
<html lang="${this.language}">
|
|
60
|
+
${this.head.toString()}
|
|
61
|
+
<body>
|
|
62
|
+
${this.body()}
|
|
63
|
+
</body>
|
|
64
|
+
</html>`;
|
|
65
|
+
}
|
|
66
|
+
allocateId(component) {
|
|
67
|
+
if (!this.componentIds) {
|
|
68
|
+
this.componentIds = [];
|
|
69
|
+
}
|
|
70
|
+
let id = Md5.hashStr(`${component.name}:${'id' in component.attributes ? component.attributes.id : `${component.path.join('/')}:${JSON.stringify(component.attributesRaw)}`}`);
|
|
71
|
+
if (this.componentIds.includes(id)) {
|
|
72
|
+
let current = component.parent;
|
|
73
|
+
do {
|
|
74
|
+
if (current === null || current.isRoot) {
|
|
75
|
+
console.error(`Could not define an unique ID for component ${component.name}, path: ${component.path}`);
|
|
76
|
+
id = randomString(16);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
id += '-' + Md5.hashStr(current.dom.outerHTML);
|
|
80
|
+
}
|
|
81
|
+
current = current?.parent || null;
|
|
82
|
+
} while (this.componentIds.includes(id));
|
|
83
|
+
}
|
|
84
|
+
this.componentIds.push(id);
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
async loadView(pathRelative, data) {
|
|
88
|
+
const viewPath = path.resolve('../' + this.application.config.components.path + '/' + pathRelative + (pathRelative.endsWith('.html') ? '' : '.html'));
|
|
89
|
+
if (!existsSync(viewPath)) {
|
|
90
|
+
console.warn(`Couldn't load document ${this.document.head.title}: ${viewPath}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const html = readFileSync(viewPath).toString();
|
|
94
|
+
await this.init(html, data);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
async loadComponent(componentName, data) {
|
|
98
|
+
const componentEntry = this.document.application.components.getByName(componentName);
|
|
99
|
+
if (componentEntry) {
|
|
100
|
+
const dataString = data === undefined ? '' : Object.keys(data).reduce((prev, key) => {
|
|
101
|
+
prev.push(`data-${key}="${attributeValueToString(key, data[key])}"`);
|
|
102
|
+
return prev;
|
|
103
|
+
}, []).join(' ');
|
|
104
|
+
await this.init(`<${componentName} ${dataString}></${componentName}>`, data);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DocumentResource } from '../Types';
|
|
2
|
+
export declare class DocumentHead {
|
|
3
|
+
title: string;
|
|
4
|
+
js: Array<DocumentResource>;
|
|
5
|
+
css: Array<DocumentResource>;
|
|
6
|
+
custom: Array<string>;
|
|
7
|
+
charset: string;
|
|
8
|
+
favicon: {
|
|
9
|
+
image: string | null;
|
|
10
|
+
type: string;
|
|
11
|
+
};
|
|
12
|
+
constructor(title: string);
|
|
13
|
+
setTitle(title: string): void;
|
|
14
|
+
add(str: string): void;
|
|
15
|
+
remove(str: string): void;
|
|
16
|
+
addJS(path: string, priority?: number, attributes?: {
|
|
17
|
+
[attributeName: string]: string | null;
|
|
18
|
+
}): DocumentResource;
|
|
19
|
+
addCSS(path: string, priority?: number, attributes?: {
|
|
20
|
+
[attributeName: string]: string | null;
|
|
21
|
+
}): DocumentResource;
|
|
22
|
+
removeJS(path: string): void;
|
|
23
|
+
removeCSS(path: string): void;
|
|
24
|
+
private toResource;
|
|
25
|
+
private attributesString;
|
|
26
|
+
toString(): string;
|
|
27
|
+
setFavicon(faviconPath: string | {
|
|
28
|
+
image: string | null;
|
|
29
|
+
type: string;
|
|
30
|
+
}): void;
|
|
31
|
+
private faviconType;
|
|
32
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export class DocumentHead {
|
|
2
|
+
constructor(title) {
|
|
3
|
+
this.js = [];
|
|
4
|
+
this.css = [];
|
|
5
|
+
this.custom = [];
|
|
6
|
+
this.charset = 'UTF-8';
|
|
7
|
+
this.favicon = {
|
|
8
|
+
image: null,
|
|
9
|
+
type: 'image/png'
|
|
10
|
+
};
|
|
11
|
+
this.title = title;
|
|
12
|
+
}
|
|
13
|
+
setTitle(title) {
|
|
14
|
+
this.title = title;
|
|
15
|
+
}
|
|
16
|
+
add(str) {
|
|
17
|
+
this.custom.push(str);
|
|
18
|
+
}
|
|
19
|
+
remove(str) {
|
|
20
|
+
this.custom = this.custom.filter((strExisting) => {
|
|
21
|
+
return strExisting !== str;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
addJS(path, priority = 0, attributes = {}) {
|
|
25
|
+
const resource = this.toResource(path, priority, attributes);
|
|
26
|
+
this.js.push(resource);
|
|
27
|
+
return resource;
|
|
28
|
+
}
|
|
29
|
+
addCSS(path, priority = 0, attributes = {}) {
|
|
30
|
+
const resource = this.toResource(path, priority, attributes);
|
|
31
|
+
this.css.push(resource);
|
|
32
|
+
return resource;
|
|
33
|
+
}
|
|
34
|
+
removeJS(path) {
|
|
35
|
+
const index = this.js.findIndex((resource) => {
|
|
36
|
+
return resource.path == path;
|
|
37
|
+
});
|
|
38
|
+
this.js.splice(index, 1);
|
|
39
|
+
}
|
|
40
|
+
removeCSS(path) {
|
|
41
|
+
const index = this.css.findIndex((resource) => {
|
|
42
|
+
return resource.path == path;
|
|
43
|
+
});
|
|
44
|
+
this.css.splice(index, 1);
|
|
45
|
+
}
|
|
46
|
+
toResource(path, priority = 0, attributes = {}) {
|
|
47
|
+
return {
|
|
48
|
+
path,
|
|
49
|
+
priority,
|
|
50
|
+
attributes
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
attributesString(resource) {
|
|
54
|
+
let attributesString = '';
|
|
55
|
+
for (const attributeName in resource.attributes) {
|
|
56
|
+
const val = resource.attributes[attributeName];
|
|
57
|
+
if (val === null) {
|
|
58
|
+
attributesString += ` ${attributeName}`;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
attributesString += ` ${attributeName}="${val}"`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return attributesString;
|
|
65
|
+
}
|
|
66
|
+
toString() {
|
|
67
|
+
const css = this.css.reduce((prev, curr) => {
|
|
68
|
+
return prev + '\n' + `<link rel="stylesheet" href="${curr.path}"${this.attributesString(curr)}>`;
|
|
69
|
+
}, '');
|
|
70
|
+
const js = this.js.reduce((prev, curr) => {
|
|
71
|
+
return prev + '\n' + `<script src="${curr.path}"${this.attributesString(curr)}></script>`;
|
|
72
|
+
}, '');
|
|
73
|
+
const custom = this.custom.reduce((prev, curr) => {
|
|
74
|
+
return prev + '\n' + curr;
|
|
75
|
+
}, '');
|
|
76
|
+
return `<head>
|
|
77
|
+
<meta charset="${this.charset}">
|
|
78
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
80
|
+
<title>${this.title}</title>
|
|
81
|
+
<link rel="icon" type="${this.favicon.type}" href="${this.favicon.image}">
|
|
82
|
+
${css}
|
|
83
|
+
${js}
|
|
84
|
+
${custom}
|
|
85
|
+
</head>`;
|
|
86
|
+
}
|
|
87
|
+
setFavicon(faviconPath) {
|
|
88
|
+
if (typeof faviconPath === 'string') {
|
|
89
|
+
this.favicon = {
|
|
90
|
+
image: faviconPath,
|
|
91
|
+
type: this.faviconType(faviconPath)
|
|
92
|
+
};
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (faviconPath.type === '') {
|
|
96
|
+
faviconPath.type = faviconPath.image ? this.faviconType(faviconPath.image) : 'image/png';
|
|
97
|
+
}
|
|
98
|
+
this.favicon = faviconPath;
|
|
99
|
+
}
|
|
100
|
+
faviconType(file) {
|
|
101
|
+
let ext = /\.([^.]+)$/.exec(file);
|
|
102
|
+
let type = 'image/png';
|
|
103
|
+
if (ext !== null) {
|
|
104
|
+
ext = ext[1].toLowerCase();
|
|
105
|
+
const types = {
|
|
106
|
+
'png': 'image/png',
|
|
107
|
+
'jpg': 'image/jpeg',
|
|
108
|
+
'jpeg': 'image/jpeg',
|
|
109
|
+
'gif': 'image/gif',
|
|
110
|
+
'ico': 'image/x-icon'
|
|
111
|
+
};
|
|
112
|
+
if (types[ext]) {
|
|
113
|
+
type = types[ext];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return type;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FormValidationEntry, PostedDataDecoded, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types';
|
|
2
|
+
export declare class FormValidation {
|
|
3
|
+
fieldRules: Array<FormValidationEntry>;
|
|
4
|
+
singleError: boolean;
|
|
5
|
+
validators: {
|
|
6
|
+
[name: string]: ValidatorFunction;
|
|
7
|
+
};
|
|
8
|
+
decorators: {
|
|
9
|
+
[validatorName: string]: ValidatorErrorDecorator;
|
|
10
|
+
};
|
|
11
|
+
addRule(fieldName: string, nameHumanReadable: string, rules: Array<string | ValidationRuleWithArguments | ValidatorFunction>): void;
|
|
12
|
+
registerValidator(name: string, validator: ValidatorFunction, decorator?: ValidatorErrorDecorator): void;
|
|
13
|
+
registerDecorator(name: string, decorator: ValidatorErrorDecorator): void;
|
|
14
|
+
validate(data: PostedDataDecoded): Promise<ValidationResult>;
|
|
15
|
+
private addError;
|
|
16
|
+
}
|