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
package/Config.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { StructuredConfig } from '@structured/Types.js';
2
+
3
+ export const config: StructuredConfig = {
4
+ // Application.importEnv will load all env variables starting with [envPrefix]_
5
+ envPrefix: 'STRUCTURED',
6
+
7
+ // whether to call Application.init when an instance of Application is created
8
+ autoInit: true,
9
+
10
+ url: {
11
+ removeTrailingSlash: true,
12
+
13
+ // if you want to enable individual component rendering set this to URI (string)
14
+ // to disable component rendering set it to false
15
+ // setting this to false disallows the use of ClientComponent.redraw and ClientComponent.add
16
+ componentRender: '/componentRender',
17
+
18
+ // function that receives the requested URL and returns boolean, if true, treat as static asset
19
+ // if there is a registered request handler that matches this same URL, it takes precedence over this
20
+ isAsset: function(uri: string) {
21
+ return uri.indexOf('/assets/') === 0;
22
+ }
23
+ },
24
+ routes: {
25
+ path: '/app/routes'
26
+ },
27
+ components : {
28
+ // relative to index.ts
29
+ path: '/app/views',
30
+
31
+ componentNameAttribute: 'structured-component'
32
+ },
33
+ session: {
34
+ cookieName: 'session',
35
+ keyLength: 24,
36
+ durationSeconds: 60 * 60,
37
+ garbageCollectIntervalSeconds: 60,
38
+ garbageCollectAfterSeconds: 500
39
+ },
40
+ http: {
41
+ port: 9191,
42
+ host: '0.0.0.0',
43
+ // used by Document.push, can be preload or preconnect
44
+ linkHeaderRel : 'preload'
45
+ },
46
+ runtime: 'Node.js'
47
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Julijan Andjelic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # Structured
2
+ Production-tested Node.js framework for creating performant **server-side rendered** web apps and APIs, with a sane amount of client side abstraction.
3
+
4
+ Framework allows the developer to develop self-contained components which are rendered server side and allows rendering a subset of components on demand. In addition to that, it includes versatile routing (including decoding of request body), session and cookie handling, easy environment variable access, form validation utilities and a templating engine (Handlebars).
5
+
6
+ It works with Node.js and Deno runtimes. Other runtimes are not tested.
7
+
8
+ - [Why Structured](#why-structured)
9
+ - [Audience](#audience)
10
+
11
+
12
+ ### Key concepts:
13
+ * [Route](#route)
14
+ * [Document](#document)
15
+ * [ClientComponent](#component) (component)
16
+
17
+ ## Route
18
+ Routes are the first thing that gets executed when your application receives a request. They are a mean for the developer to dictate what code gets executed depending on the URL. In addition to that, they allow capturing parts of the URL for use within the route.
19
+ \
20
+ \
21
+ **Simple route:**
22
+ ```
23
+ app.request.on('GET', '/hello/world', async () => {
24
+ return 'Hello, world!';
25
+ });
26
+ ```
27
+ \
28
+ **Capture URL segment:**
29
+ ```
30
+ app.request.on('GET', '/greet/(name)', async (ctx) => {
31
+ return `Hello, ${ctx.args.name}!`;
32
+ });
33
+ ```
34
+
35
+
36
+
37
+ ## Document
38
+ Document does not differ much from a component, in fact, it extends Component. It has a more user-firendly API than Component. Each Document represents a web page. It has a head and body. Structured intentionally does not differentiate between a page and a Component - page is just a component that loads many other components in a desired layout. DocumentHead (each document has one at Document.head) allows adding content to `<head>` section of the output HTML page.
39
+
40
+ Creating a document:
41
+ `const doc = new Document(app, 'HelloWorld page', ctx);`
42
+
43
+ Send document as a response:
44
+ ```
45
+ app.request.on('GET', '/home', async (ctx) => {
46
+ const doc = new Document(app, 'Home', ctx);
47
+ await doc.loadComponent('Home');
48
+ return doc;
49
+ });
50
+ ```
51
+
52
+ ## Component
53
+ A component is comprised of 1-3 files. It always must include one HTML file, while server side and client side files are optional.
54
+ * HTML file preobably requires no explanation
55
+ * server side file, code that runs on the server and makes data available to HTML and client side code
56
+ * client side file, code that runs on the client (in the browser)
57
+
58
+ You should never need to instantiate a Component on your own. You will always load a Component representing your page into a document (using `Document.loadComponent(componentName: string)`), which will know what to do from there.
59
+
60
+ Example component files:
61
+ - `/app/views/`
62
+ - `ComponentName.html`
63
+ - `ComponentName.ts`
64
+ - `ComponentName.client.ts`
65
+
66
+ It is recommended, but not necessary, that you contain each component in it's own directory:
67
+ - `/app/views/ComponentName/`
68
+ - `ComponentName.html`
69
+ - `ComponentName.ts`
70
+ - `ComponentName.client.ts`
71
+
72
+ \
73
+ **Component rules:**
74
+ - **Component names must be unique**
75
+ - Components HTML file can have a `.hbs` extension (which allows for better Handlebars sytax highlighting)
76
+ - Components can reside at any depth in the file structure
77
+
78
+ Let's create a HelloWorld Component `/app/views/HelloWorld/HelloWorld.html`:\
79
+ `Hello, World!`
80
+
81
+ Let's load this Component into a Document and send it as a response `/app/routes/HelloWorld.ts`:
82
+ ```
83
+ export default function(app: Application) {
84
+ app.request.on('GET', '/hello/world', async (ctx) => {
85
+ const doc = new Document(app, 'Hello, World! From a Component', ctx);
86
+ await doc.loadComponent('HelloWorld');
87
+ return doc;
88
+ });
89
+
90
+ // other routes here...
91
+ }
92
+ ```
93
+
94
+ You can now run the app and if you open /hello/world in the browser you will see:\
95
+ `Hello, World!` - which came from your HelloWorld component.
96
+
97
+ That was the simplest possible example, let's make it more interesting.
98
+ Create a new file `/app/views/HelloWorld/HelloWorld.ts`:
99
+ ```
100
+ import { ComponentScaffold } from 'system/Types.js';
101
+ export default class HelloWorld implements ComponentScaffold {
102
+ async getData(): Promise<{
103
+ luckyNumber: number
104
+ }> {
105
+ return {
106
+ luckyNumber: this.num()
107
+ }
108
+ }
109
+
110
+ num(): number {
111
+ return Math.floor(Math.random() * 100);
112
+ }
113
+ }
114
+ ```
115
+
116
+ Update `HelloWorld.html`:
117
+ ```
118
+ Hello, World!<br>
119
+ Your lucky number is {{luckyNumber}}
120
+ ```
121
+
122
+ Now when you access /hello/world you will see:
123
+ ```
124
+ Hello, World!
125
+ Your lucky number is [a number from 0-100]
126
+ ```
127
+
128
+ This demonstrates the use of a *server side component code* to make data available to HTML.
129
+ We just generated a random number, but the data could be anything and will more often come from a database, session, or be provided by the parent component.
130
+
131
+ Let's make it even more interesting by adding some client side code to it.
132
+ Create `/app/views/HelloWorld/HelloWorld.client.ts`:
133
+ ```
134
+ import { InitializerFunction } from 'system/Types.js';
135
+ export const init: InitializerFunction = async function() {
136
+ const generateNew = this.ref<HTMLButtonElement>('newNumber');
137
+
138
+ this.bind(generateNew, 'click', () => {
139
+ this.redraw();
140
+ });
141
+ }
142
+ ```
143
+
144
+ Update `/app/views/HelloWorld/HelloWorld.html`:
145
+ ```
146
+ Hello, World!<br>
147
+ Your lucky number is {{luckyNumber}}<br>
148
+ <button ref="newNumber">No it's not!</button>
149
+ ```
150
+
151
+ Now when you open /hello/world, the page will contain a button, when you click it, component will be redrawn and you will likely end up with a new number (unless the same random number ends up being generated, in which case you should just trust it to be your lucky number).
152
+
153
+ We've now covered all parts of a component, albeit in their simplest form. Another thing worth mentioning is that you can load other components within your components, for example:
154
+ ```
155
+ Hello, World!<br>
156
+ Your lucky number is {{luckyNumber}}<br>
157
+ <button ref="newNumber">No it's not!</button>
158
+
159
+ <AnotherComponent></AnotherComponent>
160
+ ```
161
+
162
+ This would load a Component with name `AnotherComponent` in your `HelloWorld` Component.
163
+
164
+ Passing data to child Component. Let's say we wanted to pass the `luckyNumber` to `AnotherComponent`:
165
+ ```
166
+ Hello, World!<br>
167
+ Your lucky number is {{luckyNumber}}<br>
168
+ <button ref="newNumber">No it's not!</button>
169
+
170
+ <AnotherComponent {{{attr 'number' luckyNumber}}}></AnotherComponent>
171
+ ```
172
+
173
+ That's it. `AnotherComponent` will receive the `luckyNumber` as a number, you can pass any type of data, string, number, boolean, object, array... it will be received by the child as the same type of data. However, *keep in mind the data gets serialized and de-serialized in the process, so if you pass an object to a child, it won't be a reference to the original object, rather a copy of it*.
174
+ \
175
+ Let's see how we can use the passed data within `AnotherComponent`, create `/app/views/AnotherComponent/AnotherComponent.html`:
176
+ ```
177
+ Parent says your lucky number is {{number}}.
178
+ ```
179
+
180
+ That's it. Since `AnotherComponent` has no server side code, all data passed to it is exported to HTML, hence the `number` you passed from `HelloWorld` will be readily available for use. If AnotherComponent had a server side part, the process is a bit different, it will receive it as part of the `data`, but can choose whether to make it available to the HTML, or just make use of it and return other stuff. Let's see how that works.
181
+ Create `/app/views/AnotherComponent/AnotherComponent.ts`:
182
+ ```
183
+ import { ComponentScaffold } from 'system/Types.js';
184
+ export default class AnotherComponent implements ComponentScaffold {
185
+ async getData(data: { number: number }): Promise<{
186
+ parentSuggests: number,
187
+ betterNumber: number
188
+ }> {
189
+ return {
190
+ parentSuggests: data.number,
191
+ betterNumber: data.number + 5
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ Update `/app/views/AnotherComponent/AnotherComponent.html`:\
198
+ `Parent says your lucky number is {{parentSuggests}}, but actually it is {{betterNumber}}.`
199
+
200
+ What we did is, we accepted the number provided by parent component, and returned
201
+ ```
202
+ {
203
+ parentSuggests: number,
204
+ betterNumber: number
205
+ }
206
+ ```
207
+ which is now avaialble in `AnotherComponent` HTML, we assigned the received number to `parentSuggests`, while `betterNumber` is `parentSuggests + 5`, we now have these 2 available and ready to use in our HTML template.
208
+
209
+ What about client side? **By default, data returned by server side code is not available in client side code** for obvious reasons, let's assume your server side code returns sensitive data such as user's password, you would not like that exposed on the client side, hence exporting data needs to be explicitly requested in the server side code. There are two ways to achieve this, setting `exportData = true` (exports all data), or `exportFields: Array<string> = [...keysToExport]` (export only given fields).
210
+
211
+ Let's create a client side code for `AnotherComponent` and export the `betterNumber` to it, create `/app/views/AnotherComponent/AnotherComponent.client.ts`:
212
+ ```
213
+ import { InitializerFunction } from 'system/Types.js';
214
+ export const init: InitializerFunction = async function() {
215
+ const betterNumber = this.getData<number>('betterNumber');
216
+
217
+ alert(`Did you know that your actual lucky number is ${betterNumber}?`);
218
+ }
219
+ ```
220
+
221
+ And let's update `AnotherComponent.ts` to export `betterNumber`:
222
+ ```
223
+ import { ComponentScaffold } from 'system/Types.js';
224
+ export default class AnotherComponent implements ComponentScaffold {
225
+ exportFields = ['betterNumber'];
226
+ async getData(data: { number: number }): Promise<{
227
+ parentSuggests: number,
228
+ betterNumber: number
229
+ }> {
230
+ return {
231
+ parentSuggests: data.number,
232
+ betterNumber: data.number + 5
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ The only change is we added `exportFields = ['betterNumber'];`, that's all there is to it, better number is now available to component's client side code, again, any type of data can be exported and type of data is preserved in the process.
239
+
240
+ **What about passing data from children to parent?**\
241
+ This concept is wrong to start with, if we want a component to be independent, it should not assume it's parent to exist, or behave in any specific way. That being said, components can access each other, and communicate, even from child to parent (only in client side code).
242
+
243
+ Let's say we wanted to access the `parent` Component from `AnotherComponent`:
244
+ ```
245
+ import { InitializerFunction } from 'system/Types.js';
246
+ export const init: InitializerFunction = async function() {
247
+ const betterNumber = this.getData<number>('betterNumber');
248
+
249
+ alert(`Don't listen to what ${this.parent.name} said! Your actual lucky number is ${betterNumber}?`);
250
+ }
251
+ ```
252
+ Here we accessed the `parent` and obtained it's `name`.
253
+
254
+ *"But we did not send any data to the parent here"* - correct, we did not, and we won't, instead we can inform them we have some data available, or that an event they might be interested in has ocurred, and if they care, so be it:
255
+ ```
256
+ import { InitializerFunction } from 'system/Types.js';
257
+ export const init: InitializerFunction = async function() {
258
+ const betterNumber = this.getData<number>('betterNumber');
259
+
260
+ this.emit('truth', `You lied, their lucky number is actually ${betterNumber}`);
261
+ }
262
+ ```
263
+
264
+ We emitted an `event` with `eventName` = "`truth`" and a `payload`, which in this case is a string, but can be of any type. If the parent cares about it (or for that matter, not necessarily the parent, but anyone in the component tree), they can subscribe to that event. Let's subscribe to the event from `HelloWorld` (`HelloWorld.client.ts`):
265
+ ```
266
+ import { InitializerFunction } from 'system/Types.js';
267
+ export const init: InitializerFunction = async function() {
268
+
269
+ const child = this.find('AnotherComponent'); // ClientComponent | null
270
+ if (child) {
271
+ child.on('truth', (messageBringingTruth: string) => {
272
+ console.log(`Admittedly, truth is: ${messageBringingTruth}`);
273
+ });
274
+ }
275
+
276
+ const generateNew = this.ref<HTMLButtonElement>('newNumber');
277
+
278
+ this.bind(generateNew, 'click', () => {
279
+ this.redraw();
280
+ });
281
+ }
282
+ ```
283
+
284
+ That's it. If there is `AnotherComponent` found within `HelloWorld` (which there is in our case) we are subscribing to "truth" event and capturing the payload. Payload is optional, sometimes we just want to inform anyone interested that a certain event has ocurred, without the need to pass any extra data with it. We used `this.find(componentName: string)`, this will recursively find the first instance of a component with `componentName`, optionally you can make it non-recursive by passing `false` as the second argument to `find` method in which case it will look for a direct child with given name.
285
+
286
+ We have only scratched the surface of what client-side code of a component is capable of. Which brings us to `this`. In client-side code of a component, `this` is the instance of a `ClientComponent`.
287
+
288
+ I won't list all of it's properties here, but a few notable mentions are:
289
+
290
+ Properties:
291
+ - `domNode: HTMLElement`
292
+ - `name: string`
293
+ - `parent: ClientComponent | null`
294
+ - `children: Array<ClientComponent>`
295
+ - `store: DataStoreView`
296
+
297
+ Methods:
298
+ - `getData(key?: string)` - return all data (exported by server side code of the component) if key omitted, otherwise return given key
299
+ - `setData(key: string, value: any)` - set data, which will be available server-side if component is redrawn
300
+ - `store.get<T>(key): T | undefined` - get data from client side data store (client side data storage of the component, not connected to server side data)
301
+ - `store.set(key: string, value: any)` - set data in client side data store
302
+ - `find(componentName: string, recursive: boolean = true): ClientComponent | null` - find a child component
303
+ - `findParent(componentName: string): ClientComponent | null` - find the first parent with given name
304
+ - `query(componentName: string, recursive: boolean = true): Array<ClientComponent>` - return all components with given name found within this component, if `recurive = false`, only direct children are considered
305
+ - `ref<T>(refName: string): T` - get a HTMLElement or ClientComponent that has attribute `ref="[refName]"`
306
+ - `arrayRef<T>(refName: string): Array<T>` - get an array of HTMLElement or ClientComponent that have attribute `array:ref="[refName]"`
307
+ - `add(appendTo: HTMLElement, componentName: string, data?: LooseObject)` - add `componentName` component to `appendTo` element, optionally passing `data` to the component when it's being rendered
308
+
309
+ ## Why Structured
310
+ Framework was developed by someone who has been a web developer for almost 20 years (me), and did not like the path web development has taken.
311
+ \
312
+ The whole **fragile** client-side-robot which ends up having a life of it's own, awkward ways components interact with each other and the global state, the hundreds of megabytes of toolchains to get to distribution code, the configuration of various tools which has almost become a language of their own... all that garbage - **do we really need that?** I decided it was time to <ins>rethink</ins> what we are doing, we are making web pages, and 95% of the time when we allow user to interact with the web page in the client, we are simply showing/hiding DOM nodes, updating class names of DOM nodes and doing similar, simple, stuff. Do we really need to create a fully state-aware robot just to achieve simple things like that? The answer is **no**.
313
+ **There is a better way**, which you will discover if you give Structured a try.
314
+
315
+ Above does not mean you can't create complex interactions, animations, canvas drawings or even full games within your components - nothing stops you from doing that, it's just that you don't have to, with assumption that in most cases you won't want to.
316
+
317
+ Without sacrificing speed (due to ultra-fast Structured own HTMLParser), the added benefits of the server side rendering would be:
318
+ - Page mostly usable in browsers without JS support
319
+ - SEO friendly pages
320
+ - Lean toolchain (no webpack, babel...)
321
+ - No need for a bloated client side JS framework
322
+ - No cross-browser compatibility issues
323
+ - Less client side JS resulting with a leaner page
324
+
325
+ ## Audience
326
+
327
+ The framework will be interesting to **people who actually love programming**, and are looking for a robust way to rapidly develop their web applications, while being able to **enjoy** the process once again.
328
+
329
+ It will probably primarily be interesting to old-school web developers (*those of you who ever created a border radius using 4 images of rounded corners because CSS did not yet have border-radius, I'm talking about you here*). Especially if you are a firm believer in **type-safe code**, and agree that we all were writing compiled code for years by writing JavaScript, and get a bad gut feeling when our code needs to go through an enormous toolchain, in hope that it will still work as intended after all that.
330
+ \
331
+ \
332
+ However, I also hope some of the **new programmers** who did not yet get caught in the whole robot-client-side will give it a chance and save themselves from having to invest months in learning various toolchains and wasting their life on setting up config files, especially those who recognized the power of type-safe languages, or **come from a type-safe language to web development**.
package/app/Types.ts ADDED
@@ -0,0 +1 @@
1
+ export type RequestContextData = {}
@@ -0,0 +1,9 @@
1
+ You don't have to use this directory, but if you are keen on MVC concept, this would be the place for your models.
2
+
3
+ There are not specific rules for these files. They are never loaded automaticallt by the framework, instead you should
4
+ load them where you want to use them.
5
+
6
+ Usually a model will export a class or a class instance, but it could be a function, set of functions, or anything really.
7
+
8
+ Models are meant to abstract the logic and data layer for a specific entity of your app. For example a web shop would likely
9
+ have a model Product.ts which exports a class with methods such as create(productData: ProductData) to create a new product and delete(productId: number) to delete an existing product.
@@ -0,0 +1,19 @@
1
+ Routes:
2
+
3
+ You can add routes directly in index.ts using app.request.on
4
+ but in order to avoid clutter and keep the code structured, you should add the routes here (/app/routes)
5
+
6
+ All files in this directory will be loaded once the server is started,
7
+ so feel free to separate your routes in as many files as you feel makes sense, eg. it would make sense
8
+ to have Auth.ts that would add routes for everything auth related, such as /login, /register, etc...
9
+
10
+ Example:
11
+
12
+ export default async function(app: Application) {
13
+
14
+ app.request.on('GET', '/login', async (ctx) => {
15
+ // ctx is a RequestContext
16
+ ctx.response.write('Login page');
17
+ });
18
+
19
+ }
@@ -0,0 +1 @@
1
+ Place for pages and components
@@ -0,0 +1 @@
1
+ {{{layoutComponent component data attributes}}}
package/bin/structured ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Structured init script
4
+ // meant to be executed after installing Structured framework to set up basic boilerplate
5
+ import { resolve } from 'path';
6
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+
8
+ const cwd = resolve(import.meta.dirname, '..');
9
+ const projectRoot = resolve('.');
10
+
11
+ if (process.argv.length < 3) {
12
+ // executed with no commands
13
+ console.log('Thanks for using the Structured framework.');
14
+ console.log(`To set up a basic boilerplate in ${projectRoot} run:\n npx structured init`);
15
+ process.exit();
16
+ }
17
+
18
+
19
+ const command = process.argv[2];
20
+
21
+ function copyIfNotExists(src, dst) {
22
+ if (typeof dst !== 'string') {
23
+ dst = src;
24
+ }
25
+ if (existsSync(`${cwd}/${src}`) && ! existsSync(`${projectRoot}/${dst}`)) {
26
+ console.log(`Creating ${dst}`);
27
+ cpSync(`${cwd}/${src}`, `${projectRoot}/${dst}`);
28
+ }
29
+ }
30
+
31
+ function createDir(path) {
32
+ if (! existsSync(`${projectRoot}/${path}`)) {
33
+ console.log(`Creating directory ${projectRoot}/${path}`);
34
+ mkdirSync(`${projectRoot}/${path}`);
35
+ }
36
+ }
37
+
38
+ function createTsconfig() {
39
+ const tsconfigPath = `${projectRoot}/tsconfig.json`;
40
+ const exists = existsSync(tsconfigPath);
41
+
42
+ const paths = {
43
+ "/assets/ts/*": ["./assets/ts/*"],
44
+ "/assets/client-js/*": ["./system/*"],
45
+ "@structured/*": [
46
+ "./node_modules/structured/build/system/server/*",
47
+ "./node_modules/structured/build/system/client/*",
48
+ "./node_modules/structured/build/system/*",
49
+ "./system/server/*",
50
+ "./system/client/*",
51
+ "./system/*",
52
+ ]
53
+ }
54
+
55
+ if (exists) {
56
+ console.log('Updating tsconfig.json, adding @structured to compilerOptions.paths');
57
+ // tsconfig exists, add @structured paths
58
+ const config = JSON.parse(readFileSync(tsconfigPath).toString());
59
+
60
+ if (! config.compilerOptions) {
61
+ config.compilerOptions = {}
62
+ }
63
+
64
+ if (! config.compilerOptions.paths) {
65
+ config.compilerOptions.paths = paths;
66
+ }
67
+
68
+ writeFileSync(tsconfigPath, JSON.stringify(config, null, 4));
69
+ } else {
70
+ console.log('Creating tsconfig.json');
71
+ // tsconfig does not exist, create it
72
+ const config = {
73
+ "compilerOptions": {
74
+ "noImplicitAny": true,
75
+ "noUnusedLocals": true,
76
+ "noImplicitReturns": true,
77
+ "alwaysStrict": true,
78
+ "strictNullChecks": true,
79
+ "strictPropertyInitialization": true,
80
+ "strictBindCallApply": true,
81
+ "moduleResolution" : "Node",
82
+ "outDir": "./build",
83
+ "module": "ES2020",
84
+ "target": "ES2021",
85
+ "allowSyntheticDefaultImports": true, // albe to do import { default as varname } from 'module'
86
+ "preserveSymlinks": true,
87
+ "removeComments": true,
88
+ "baseUrl": ".",
89
+ "rootDir": ".",
90
+ paths
91
+ },
92
+ "include": ["./system/**/*", "./app/**/*", "./assets/ts/**/*", "index.ts"]
93
+ }
94
+
95
+ writeFileSync(tsconfigPath, JSON.stringify(config, null, 4));
96
+ }
97
+ }
98
+
99
+ if (command === 'init') {
100
+ console.log('Setting up a basic Structured boilerplate...');
101
+ copyIfNotExists('index.ts');
102
+ copyIfNotExists('Config.ts');
103
+ createDir('app');
104
+ createDir('app/routes');
105
+ createDir('app/views');
106
+ createDir('app/models');
107
+ createDir('app/lib');
108
+
109
+ createTsconfig();
110
+
111
+ console.log(`Structured initialized, you can run "tsc" to build`);
112
+ } else {
113
+ console.log(`Command "${command}" not recognized`);
114
+ }
@@ -0,0 +1,2 @@
1
+ import { StructuredConfig } from '@structured/Types.js';
2
+ export declare const config: StructuredConfig;
@@ -0,0 +1,31 @@
1
+ export const config = {
2
+ envPrefix: 'STRUCTURED',
3
+ autoInit: true,
4
+ url: {
5
+ removeTrailingSlash: true,
6
+ componentRender: '/componentRender',
7
+ isAsset: function (uri) {
8
+ return uri.indexOf('/assets/') === 0;
9
+ }
10
+ },
11
+ routes: {
12
+ path: '/app/routes'
13
+ },
14
+ components: {
15
+ path: '/app/views',
16
+ componentNameAttribute: 'structured-component'
17
+ },
18
+ session: {
19
+ cookieName: 'session',
20
+ keyLength: 24,
21
+ durationSeconds: 60 * 60,
22
+ garbageCollectIntervalSeconds: 60,
23
+ garbageCollectAfterSeconds: 500
24
+ },
25
+ http: {
26
+ port: 9191,
27
+ host: '0.0.0.0',
28
+ linkHeaderRel: 'preload'
29
+ },
30
+ runtime: 'Node.js'
31
+ };
@@ -0,0 +1 @@
1
+ export type RequestContextData = {};
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,2 @@
1
+ import { Application } from '../../system/server/Application.js';
2
+ export default function (app: Application): void;