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.
Files changed (129) hide show
  1. package/Config.ts +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +332 -0
  4. package/app/Types.ts +1 -0
  5. package/app/models/README.md +9 -0
  6. package/app/routes/README.md +19 -0
  7. package/app/views/README.md +1 -0
  8. package/app/views/layout.html +1 -0
  9. package/bin/structured +114 -0
  10. package/build/Config.d.ts +2 -0
  11. package/build/Config.js +31 -0
  12. package/build/app/Types.d.ts +1 -0
  13. package/build/app/Types.js +1 -0
  14. package/build/app/models/Users.d.ts +0 -0
  15. package/build/app/models/Users.js +1 -0
  16. package/build/app/routes/Auth.d.ts +0 -0
  17. package/build/app/routes/Auth.js +1 -0
  18. package/build/app/routes/Test.d.ts +2 -0
  19. package/build/app/routes/Test.js +101 -0
  20. package/build/app/routes/Todo.d.ts +0 -0
  21. package/build/app/routes/Todo.js +1 -0
  22. package/build/app/routes/Upload.d.ts +0 -0
  23. package/build/app/routes/Upload.js +1 -0
  24. package/build/app/routes/Validation.d.ts +2 -0
  25. package/build/app/routes/Validation.js +34 -0
  26. package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
  27. package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
  28. package/build/app/views/components/ClientImport/Export.d.ts +1 -0
  29. package/build/app/views/components/ClientImport/Export.js +1 -0
  30. package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
  31. package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
  32. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
  33. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
  34. package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
  35. package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
  36. package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
  37. package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
  38. package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
  39. package/build/app/views/components/PassObject/PassObject.js +10 -0
  40. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
  41. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
  42. package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
  43. package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
  44. package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
  45. package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
  46. package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
  47. package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
  48. package/build/assets/ts/Export.d.ts +1 -0
  49. package/build/assets/ts/Export.js +1 -0
  50. package/build/index.d.ts +1 -0
  51. package/build/index.js +3 -0
  52. package/build/system/Helpers.d.ts +3 -0
  53. package/build/system/Helpers.js +72 -0
  54. package/build/system/Symbols.d.ts +3 -0
  55. package/build/system/Symbols.js +3 -0
  56. package/build/system/Types.d.ts +171 -0
  57. package/build/system/Types.js +1 -0
  58. package/build/system/Util.d.ts +20 -0
  59. package/build/system/Util.js +336 -0
  60. package/build/system/client/App.d.ts +7 -0
  61. package/build/system/client/App.js +8 -0
  62. package/build/system/client/Client.d.ts +6 -0
  63. package/build/system/client/Client.js +9 -0
  64. package/build/system/client/ClientComponent.d.ts +68 -0
  65. package/build/system/client/ClientComponent.js +734 -0
  66. package/build/system/client/DataStore.d.ts +22 -0
  67. package/build/system/client/DataStore.js +64 -0
  68. package/build/system/client/DataStoreView.d.ts +19 -0
  69. package/build/system/client/DataStoreView.js +56 -0
  70. package/build/system/client/EventEmitter.d.ts +7 -0
  71. package/build/system/client/EventEmitter.js +31 -0
  72. package/build/system/client/Net.d.ts +13 -0
  73. package/build/system/client/Net.js +39 -0
  74. package/build/system/client/NetRequest.d.ts +13 -0
  75. package/build/system/client/NetRequest.js +45 -0
  76. package/build/system/server/Application.d.ts +31 -0
  77. package/build/system/server/Application.js +171 -0
  78. package/build/system/server/Component.d.ts +27 -0
  79. package/build/system/server/Component.js +249 -0
  80. package/build/system/server/Components.d.ts +12 -0
  81. package/build/system/server/Components.js +77 -0
  82. package/build/system/server/Cookies.d.ts +6 -0
  83. package/build/system/server/Cookies.js +19 -0
  84. package/build/system/server/Document.d.ts +24 -0
  85. package/build/system/server/Document.js +107 -0
  86. package/build/system/server/DocumentHead.d.ts +32 -0
  87. package/build/system/server/DocumentHead.js +118 -0
  88. package/build/system/server/FormValidation.d.ts +16 -0
  89. package/build/system/server/FormValidation.js +197 -0
  90. package/build/system/server/Handlebars.d.ts +11 -0
  91. package/build/system/server/Handlebars.js +34 -0
  92. package/build/system/server/Request.d.ts +21 -0
  93. package/build/system/server/Request.js +356 -0
  94. package/build/system/server/Session.d.ts +23 -0
  95. package/build/system/server/Session.js +114 -0
  96. package/build/system/server/dom/DOMFragment.d.ts +4 -0
  97. package/build/system/server/dom/DOMFragment.js +6 -0
  98. package/build/system/server/dom/DOMNode.d.ts +31 -0
  99. package/build/system/server/dom/DOMNode.js +110 -0
  100. package/build/system/server/dom/HTMLParser.d.ts +21 -0
  101. package/build/system/server/dom/HTMLParser.js +204 -0
  102. package/index.ts +4 -0
  103. package/package.json +31 -0
  104. package/system/Helpers.ts +97 -0
  105. package/system/Symbols.ts +6 -0
  106. package/system/Types.ts +234 -0
  107. package/system/Util.ts +488 -0
  108. package/system/client/App.ts +11 -0
  109. package/system/client/Client.ts +9 -0
  110. package/system/client/ClientComponent.ts +1117 -0
  111. package/system/client/DataStore.ts +101 -0
  112. package/system/client/DataStoreView.ts +82 -0
  113. package/system/client/EventEmitter.ts +38 -0
  114. package/system/client/Net.ts +58 -0
  115. package/system/client/NetRequest.ts +64 -0
  116. package/system/server/Application.ts +230 -0
  117. package/system/server/Component.ts +404 -0
  118. package/system/server/Components.ts +111 -0
  119. package/system/server/Cookies.ts +29 -0
  120. package/system/server/Document.ts +163 -0
  121. package/system/server/DocumentHead.ts +150 -0
  122. package/system/server/FormValidation.ts +231 -0
  123. package/system/server/Handlebars.ts +51 -0
  124. package/system/server/Request.ts +497 -0
  125. package/system/server/Session.ts +151 -0
  126. package/system/server/dom/DOMFragment.ts +7 -0
  127. package/system/server/dom/DOMNode.ts +140 -0
  128. package/system/server/dom/HTMLParser.ts +238 -0
  129. 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
+ }