structured-fw 1.4.0 → 1.6.0

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 (32) hide show
  1. package/README.md +64 -23
  2. package/build/system/EventEmitter.js +4 -6
  3. package/build/system/StructuredError.d.ts +8 -0
  4. package/build/system/StructuredError.js +40 -0
  5. package/build/system/bin/structured.js +1 -2
  6. package/build/system/client/ClientApplication.js +3 -2
  7. package/build/system/client/ClientComponent.js +21 -13
  8. package/build/system/client/DataStore.js +2 -4
  9. package/build/system/client/DataStoreView.js +3 -1
  10. package/build/system/client/NetRequest.js +7 -2
  11. package/build/system/server/Application.d.ts +3 -2
  12. package/build/system/server/Application.js +19 -7
  13. package/build/system/server/Component.js +15 -7
  14. package/build/system/server/Components.js +3 -2
  15. package/build/system/server/Document.d.ts +1 -1
  16. package/build/system/server/Document.js +10 -7
  17. package/build/system/server/DocumentHead.js +9 -8
  18. package/build/system/server/FormValidation.js +121 -123
  19. package/build/system/server/Handlebars.js +2 -4
  20. package/build/system/server/Layout.d.ts +2 -2
  21. package/build/system/server/Layout.js +4 -0
  22. package/build/system/server/Request.d.ts +1 -11
  23. package/build/system/server/Request.js +12 -269
  24. package/build/system/server/RequestContext.d.ts +43 -0
  25. package/build/system/server/RequestContext.js +322 -0
  26. package/build/system/server/Session.js +3 -2
  27. package/build/system/server/dom/DOMNode.js +11 -7
  28. package/build/system/server/dom/HTMLParser.js +10 -8
  29. package/build/system/types/application.types.d.ts +1 -1
  30. package/build/system/types/component.types.d.ts +1 -1
  31. package/build/system/types/request.types.d.ts +1 -19
  32. package/package.json +2 -1
package/README.md CHANGED
@@ -29,6 +29,13 @@ npm init -y
29
29
  npm install @types/node
30
30
  ```
31
31
 
32
+ ## Set type to "module" in package.json
33
+ ```
34
+ ...
35
+ "type": "module",
36
+ ...
37
+ ```
38
+
32
39
  *If you have TypeScript installed globally then you can skip the following*\
33
40
  `npm install --save-dev typescript`
34
41
 
@@ -127,13 +134,14 @@ new Application(config);
127
134
  - `serverStarted` - executed once the built-in http server is started and running. Callback receives Server (exported from node:http) instance as the first argument
128
135
  - `beforeRequestHandler` - runs before any request handler (route) is executed. Callback receives `RequestContext` as the first argument. Useful for example to set `RequestContext.data: RequestContextData` (user defined data, to make it available to routes and components)
129
136
  - `afterRequestHandler` - runs after any request handler (route) is executed. Callback receives `RequestContext` as the first argument
137
+ - `requestHandleError` - runs if there were errors while serving the request. Callback's result is sent as a response - a good use case is showing a "server error" page. Callback receives `RequestContext` as the first argument
130
138
  - `afterRoutes` - runs after all routes are loaded from `StructuredConfig.routes.path`. Callback receives no arguments
131
139
  - `beforeComponentsLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
132
140
  - `afterComponentsLoaded` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives instance of Components as the first argument
133
141
  - `documentCreated` - runs whenever an instance of a [Document](#document) is created. Callback receives the Document instance as the first argument. You will often use this, for example if you want to include a CSS file to all pages `Document.head.addCSS(...)`
134
142
  - `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
135
143
  - `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
136
- - `pageNotFound` - runs when a request is received for which there is no registered request handler (route), and the requested URL is not an asset. Callback receives `RequestContext` as the first argument
144
+ - `pageNotFound` - runs when a request is received for which there is no registered request handler (route), and the requested URL is not an asset. Callback's result is sent as a response - a good use case is showing a 404 page. Callback receives `RequestContext` as the first argument
137
145
  - **Callback to any of the `ApplicationEvents` is expected to be an async function**
138
146
  - `importEnv<T extends LooseObject>(smartPrimitives: boolean = true): T` - import ENV variables that start with `StructuredConfig.envPrefix`_ (if envPrefix is omitted from config, all ENV variables are returned). It is a generic method so that you can specify the expected return type. If `smartPrimitives = true` importEnv will convert the ENV values to type it feels is appropriate:
139
147
  - numeric values -> `number`
@@ -258,44 +266,67 @@ Route file name has no effect on how the route (request handler) behaves, the on
258
266
  RequestContext is created for every received request. It contains all the data related to the request, as well as data you include in `RequestContextData`.
259
267
  All request handlers receive a `RequestContext` as the first argument. Your component server side part also receives `RequestContext` as the second argument.
260
268
  ```
261
- type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
262
- request: IncomingMessage,
263
- response: ServerResponse,
264
- args: URIArguments,
265
- handler: null|RequestHandler,
269
+ class RequestContext<Body extends LooseObject | undefined = LooseObject> = {
270
+ app: Application;
271
+ uri: string;
272
+
273
+ request: IncomingMessage;
274
+ response: ServerResponse;
266
275
 
267
- cookies: Record<string, string>,
276
+ // captured URI arguments
277
+ // for example if the requested URI was /users/8 and the pattern was /users/(userId:num)
278
+ // args will be { userId: 8 }
279
+ args: URIArguments;
280
+
281
+ // request cookies parsed into an object
282
+ cookies: Record<string, string>;
268
283
 
269
284
  // POSTed data, parsed to object
270
- body?: LooseObject,
285
+ body?: LooseObject;
271
286
 
272
- bodyRaw?: Buffer,
287
+ bodyRaw?: Buffer;
273
288
 
274
289
  // files extracted from request body
275
- files?: Record<string, RequestBodyRecordValue>,
290
+ files?: Record<string, RequestBodyRecordValue>;
276
291
 
277
292
  // user defined data
278
- data: RequestContextData,
293
+ data: RequestContextData;
279
294
 
280
- // if session is started and user has visited any page
281
- sessionId?: string,
295
+ // defined if session is started and user has visited any page
296
+ sessionId?: string;
282
297
 
283
- // true if x-requested-with header is received and it equals 'xmlhttprequest'
284
- isAjax: boolean,
298
+ // true if x-requested-with header value is 'xmlhttprequest'
299
+ isAjax: () => boolean;
285
300
 
286
301
  // time when request was received (unix timestamp in milliseconds)
287
- timeStart: number,
302
+ timeStart: number;
303
+
304
+ // URL GET arguments, for example if the URI isss /users?id=8
305
+ // getArgs would be { id: '8' }
306
+ getArgs: PostedDataDecoded;
307
+
308
+ // create a Document and load given component
309
+ createDocument: (title: string, component: string, data?: LooseObject) => Promise<Document>;
288
310
 
289
- // URL GET arguments
290
- getArgs: PostedDataDecoded,
311
+ // create a Document using provided layout
312
+ layoutDocument: layoutDocument(
313
+ layout: Layout,
314
+ title: string,
315
+ component: string,
316
+ data?: LooseObject,
317
+ attributes?: Record<string, string>
318
+ ) => Promise<Document>;
291
319
 
292
320
  // send given data as a response
293
- respondWith: (data: any) => Promise<void>,
321
+ respondWith: (data: any) => Promise<void>;
294
322
 
295
- // redirect to given url, with given statusCode
296
- redirect: (to: string, statusCode?: number) => void,
323
+ // redirect to given URI, with given statusCode (default 302)
324
+ redirect: (to: string, statusCode?: number) => void;
297
325
 
298
- // show a 404 page
326
+ // show a blank page and send a 404 status code
327
+ // to show a custom page, text, json, etc... register an event handler for pageNotFound event
328
+ // for example:
329
+ // app.on('pageNotFound', (ctx) => { return await ctx.createDocument('Page not found', 'NotFound'); })
299
330
  show404: () => Promise<void>
300
331
  }
301
332
  ```
@@ -371,6 +402,15 @@ app.request.on('GET', '/home', async (ctx) => {
371
402
  });
372
403
  ```
373
404
 
405
+ Above code could be simplified using RequestContext.createDocument method as:
406
+ ```
407
+ app.request.on('GET', '/home', async (ctx) => {
408
+ return await ctx.createDocument('Home', 'Home');
409
+ });
410
+ ```
411
+
412
+ Much shorter, but slightly confusing due to bad example (both arguments being 'Home'). First argument is the document title, second argument is the name of the component you want to load.
413
+
374
414
  > [!TIP]
375
415
  > Since version 0.8.4 Document extends EventEmitter, and "componentCreated" event is emitted whenever a component instance is created within the Document.\
376
416
  > This makes the following possible:
@@ -445,7 +485,8 @@ That was the simplest possible example, let's make it more interesting by adding
445
485
  Create a new file `/app/views/HelloWorld/HelloWorld.ts` (server side component code):
446
486
  ```
447
487
  import { Application } from 'structured-fw/Application';
448
- import { ComponentScaffold, RequestContext } from 'structured-fw/Types';
488
+ import { ComponentScaffold } from 'structured-fw/Types';
489
+ import { RequestContext } from 'structured-fw/RequestContext';
449
490
 
450
491
  type ComponentInput = {
451
492
  name: string,
@@ -1,10 +1,8 @@
1
1
  export class EventEmitter {
2
- constructor() {
3
- this.listeners = {};
4
- this.destroyed = false;
5
- this.ready = false;
6
- this.eventQueue = [];
7
- }
2
+ listeners = {};
3
+ destroyed = false;
4
+ ready = false;
5
+ eventQueue = [];
8
6
  on(eventName, callback) {
9
7
  if (this.destroyed) {
10
8
  return;
@@ -0,0 +1,8 @@
1
+ export declare class StructuredError extends Error {
2
+ causedBy: StructuredError | null;
3
+ stack: string | undefined;
4
+ constructor(message: string | Error, cause: StructuredError | Error | null);
5
+ toString(depth?: number): string;
6
+ log(depth?: number): void;
7
+ private formatLines;
8
+ }
@@ -0,0 +1,40 @@
1
+ export class StructuredError extends Error {
2
+ causedBy;
3
+ stack;
4
+ constructor(message, cause) {
5
+ super(typeof message === 'string' ? message : message.message);
6
+ if (message instanceof Error) {
7
+ this.stack = message.stack;
8
+ }
9
+ if (cause === null) {
10
+ this.causedBy = null;
11
+ }
12
+ else {
13
+ this.causedBy = cause instanceof StructuredError ? cause : new StructuredError(cause, null);
14
+ }
15
+ }
16
+ toString(depth = 0) {
17
+ const errorParts = [this.message];
18
+ if (this.stack) {
19
+ errorParts.push('Stack trace:');
20
+ errorParts.push(this.stack);
21
+ }
22
+ if (this.causedBy !== null) {
23
+ errorParts.push('Originated from:');
24
+ errorParts.push(this.causedBy.toString(depth + 1));
25
+ }
26
+ return this.formatLines(errorParts.join('\n'), depth);
27
+ }
28
+ log(depth = 0) {
29
+ console.error(this.formatLines(this.toString(), depth));
30
+ }
31
+ formatLines(text, depth) {
32
+ const linesFormatted = [];
33
+ const lines = text.split('\n');
34
+ const prepend = ' '.repeat(depth);
35
+ lines.forEach((line) => {
36
+ linesFormatted.push(prepend + line);
37
+ });
38
+ return linesFormatted.join('\n');
39
+ }
40
+ }
@@ -64,11 +64,10 @@ function createTsconfig() {
64
64
  "moduleResolution": "node16",
65
65
  "outDir": "./build",
66
66
  "module": "Node16",
67
- "target": "ES2021",
67
+ "target": "es2022",
68
68
  "allowSyntheticDefaultImports": true,
69
69
  "preserveSymlinks": true,
70
70
  "removeComments": true,
71
- "baseUrl": ".",
72
71
  "rootDir": ".",
73
72
  paths
74
73
  },
@@ -1,9 +1,10 @@
1
1
  import { DataStore } from './DataStore.js';
2
2
  import { ClientComponent } from './ClientComponent.js';
3
3
  export class ClientApplication {
4
+ root;
5
+ store = new DataStore();
6
+ initializers = {};
4
7
  constructor() {
5
- this.store = new DataStore();
6
- this.initializers = {};
7
8
  this.loadInitializers();
8
9
  this.root = new ClientComponent(null, 'root', document.body, this);
9
10
  }
@@ -4,21 +4,29 @@ import { Net } from './Net.js';
4
4
  import { NetRequest } from './NetRequest.js';
5
5
  import { EventEmitter } from '../EventEmitter.js';
6
6
  export class ClientComponent extends EventEmitter {
7
+ name;
8
+ children = [];
9
+ parent;
10
+ domNode;
11
+ isRoot;
12
+ root;
13
+ store;
14
+ app;
15
+ net = new Net();
16
+ initializerExecuted = false;
17
+ fn;
18
+ destroyed = false;
19
+ redrawRequest = null;
20
+ bound = [];
21
+ conditionals = [];
22
+ conditionalCallbacks = {};
23
+ conditionalClassNames = [];
24
+ refs = {};
25
+ refsArray = {};
26
+ isReady = false;
27
+ data = {};
7
28
  constructor(parent, name, domNode, app) {
8
29
  super();
9
- this.children = [];
10
- this.net = new Net();
11
- this.initializerExecuted = false;
12
- this.destroyed = false;
13
- this.redrawRequest = null;
14
- this.bound = [];
15
- this.conditionals = [];
16
- this.conditionalCallbacks = {};
17
- this.conditionalClassNames = [];
18
- this.refs = {};
19
- this.refsArray = {};
20
- this.isReady = false;
21
- this.data = {};
22
30
  this.name = name;
23
31
  this.domNode = domNode;
24
32
  if (parent === null) {
@@ -1,9 +1,7 @@
1
1
  import { equalDeep } from '../Util.js';
2
2
  export class DataStore {
3
- constructor() {
4
- this.data = {};
5
- this.changeListeners = {};
6
- }
3
+ data = {};
4
+ changeListeners = {};
7
5
  set(component, key, val, force = false, triggerListeners = true) {
8
6
  const componentId = component.getData('componentId');
9
7
  const oldValue = this.get(componentId, key);
@@ -1,6 +1,8 @@
1
1
  export class DataStoreView {
2
+ store;
3
+ component;
4
+ destroyed = false;
2
5
  constructor(store, component) {
3
- this.destroyed = false;
4
6
  this.store = store;
5
7
  this.component = component;
6
8
  }
@@ -1,7 +1,12 @@
1
1
  export class NetRequest {
2
+ xhr = new XMLHttpRequest();
3
+ method;
4
+ url;
5
+ headers;
6
+ responseType;
7
+ body;
8
+ requestSent = false;
2
9
  constructor(method, url, headers = {}, responseType = 'text', body) {
3
- this.xhr = new XMLHttpRequest();
4
- this.requestSent = false;
5
10
  this.method = method;
6
11
  this.url = url;
7
12
  this.headers = headers;
@@ -2,13 +2,13 @@ import { Server } from 'node:http';
2
2
  import { ApplicationEvents } from '../types/application.types.js';
3
3
  import { StructuredConfig } from '../types/structured.types.js';
4
4
  import { LooseObject } from '../types/general.types.js';
5
- import { RequestContext } from "../types/request.types.js";
6
5
  import { Document } from './Document.js';
7
6
  import { Components } from './Components.js';
8
7
  import { Session } from './Session.js';
9
8
  import { Request } from './Request.js';
10
9
  import { Handlebars } from './Handlebars.js';
11
10
  import { Cookies } from './Cookies.js';
11
+ import { RequestContext } from './RequestContext.js';
12
12
  export declare class Application {
13
13
  readonly config: StructuredConfig;
14
14
  initialized: boolean;
@@ -25,13 +25,14 @@ export declare class Application {
25
25
  constructor(config: StructuredConfig);
26
26
  init(): Promise<void>;
27
27
  private start;
28
- on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext : E extends 'documentCreated' ? Document : E extends 'afterComponentsLoaded' ? Components : E extends 'serverStarted' ? Server : undefined) => void): void;
28
+ on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' | 'requestHandleError' ? RequestContext<RequestContextData> : E extends 'documentCreated' ? Document : E extends 'afterComponentsLoaded' ? Components : E extends 'serverStarted' ? Server : undefined) => void): void;
29
29
  emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>>;
30
30
  importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
31
31
  exportContextFields(...fields: Array<keyof RequestContextData>): void;
32
32
  contentType(extension: string): string | false;
33
33
  registerPlugin<Opt extends Readonly<LooseObject>>(callback: (app: Application, options: Opt) => void | Promise<void>, opts: NoInfer<Opt>): Promise<void>;
34
34
  private respondWithComponent;
35
+ directory(uri?: string): string;
35
36
  memoryUsage(): NodeJS.MemoryUsage;
36
37
  printMemoryUsage(): void;
37
38
  }
@@ -11,14 +11,19 @@ import { Request } from './Request.js';
11
11
  import { Handlebars } from './Handlebars.js';
12
12
  import { Cookies } from './Cookies.js';
13
13
  export class Application {
14
+ config;
15
+ initialized = false;
16
+ server = null;
17
+ listening = false;
18
+ eventEmitter = new EventEmitter();
19
+ cookies;
20
+ session;
21
+ request;
22
+ components;
23
+ handlebars = new Handlebars();
24
+ exportedRequestContextData = [];
25
+ data = {};
14
26
  constructor(config) {
15
- this.initialized = false;
16
- this.server = null;
17
- this.listening = false;
18
- this.eventEmitter = new EventEmitter();
19
- this.handlebars = new Handlebars();
20
- this.exportedRequestContextData = [];
21
- this.data = {};
22
27
  this.config = config;
23
28
  this.cookies = new Cookies();
24
29
  this.session = new Session(this);
@@ -167,6 +172,13 @@ export class Application {
167
172
  }
168
173
  return false;
169
174
  }
175
+ directory(uri) {
176
+ const dir = path.resolve('../');
177
+ if (typeof uri === 'string') {
178
+ return path.resolve(dir, uri.startsWith('/') ? uri.substring(1) : uri);
179
+ }
180
+ return dir;
181
+ }
170
182
  memoryUsage() {
171
183
  return process.memoryUsage();
172
184
  }
@@ -2,14 +2,22 @@ import { Document } from './Document.js';
2
2
  import { attributeValueFromString, attributeValueToString, objectEach, toCamelCase } from '../Util.js';
3
3
  import { DOMFragment } from './dom/DOMFragment.js';
4
4
  import { EventEmitter } from '../EventEmitter.js';
5
+ import { StructuredError } from '../StructuredError.js';
5
6
  export class Component extends EventEmitter {
7
+ id;
8
+ name;
9
+ document;
10
+ parent;
11
+ children = [];
12
+ path = [];
13
+ attributesRaw = {};
14
+ attributes = {};
15
+ dom;
16
+ data = {};
17
+ entry;
18
+ isRoot;
6
19
  constructor(name, node, parent, autoInit = true) {
7
20
  super();
8
- this.children = [];
9
- this.path = [];
10
- this.attributesRaw = {};
11
- this.attributes = {};
12
- this.data = {};
13
21
  const isDocument = this instanceof Document;
14
22
  this.name = name;
15
23
  this.emitterReady();
@@ -96,7 +104,7 @@ export class Component extends EventEmitter {
96
104
  await this.entry.serverPart.getData(Object.assign(importedParentData, this.attributes, data || {}), this.document.ctx, this.document.application, this) : {}) || {};
97
105
  }
98
106
  catch (e) {
99
- throw new Error(`Error executing getData in component ${this.name}: ${e.message}`);
107
+ throw new StructuredError(`Error executing getData in component ${this.name}`, e);
100
108
  }
101
109
  if (data === undefined) {
102
110
  if (this.entry && this.entry.hasServerPart) {
@@ -271,7 +279,7 @@ export class Component extends EventEmitter {
271
279
  this.dom.innerHTML = this.document.application.handlebars.compile(html, data);
272
280
  }
273
281
  catch (e) {
274
- throw new Error(`Error compiling Handlebars template in component ${this.name}, error: ${e.message}`);
282
+ throw new StructuredError(`Error compiling Handlebars template in component ${this.name}`, e);
275
283
  }
276
284
  }
277
285
  }
@@ -2,9 +2,10 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { stripBOM } from '../Util.js';
4
4
  export class Components {
5
+ config;
6
+ components = {};
7
+ componentNames = [];
5
8
  constructor(app) {
6
- this.components = {};
7
- this.componentNames = [];
8
9
  this.config = app.config;
9
10
  }
10
11
  loadComponents(relativeToPath) {
@@ -1,10 +1,10 @@
1
1
  import { ServerResponse } from 'node:http';
2
2
  import { LooseObject } from '../types/general.types.js';
3
3
  import { Initializers } from '../types/component.types.js';
4
- import { RequestContext } from "../types/request.types.js";
5
4
  import { Application } from './Application.js';
6
5
  import { DocumentHead } from './DocumentHead.js';
7
6
  import { Component } from './Component.js';
7
+ import { RequestContext } from './RequestContext.js';
8
8
  export declare class Document extends Component<{
9
9
  'componentCreated': Component;
10
10
  'beforeRender': void;
@@ -5,15 +5,18 @@ import path from 'node:path';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  export class Document extends Component {
8
+ head;
9
+ language = 'en';
10
+ application;
11
+ htmlTagAttributes = {};
12
+ bodyTagAttributes = {};
13
+ initializers = {};
14
+ initializersInitialized = false;
15
+ componentIds = [];
16
+ ctx;
17
+ appendHTML = '';
8
18
  constructor(app, title, ctx) {
9
19
  super('root');
10
- this.language = 'en';
11
- this.htmlTagAttributes = {};
12
- this.bodyTagAttributes = {};
13
- this.initializers = {};
14
- this.initializersInitialized = false;
15
- this.componentIds = [];
16
- this.appendHTML = '';
17
20
  this.application = app;
18
21
  this.ctx = ctx;
19
22
  this.document = this;
@@ -1,13 +1,14 @@
1
1
  export class DocumentHead {
2
+ title;
3
+ js = [];
4
+ css = [];
5
+ custom = [];
6
+ charset = 'UTF-8';
7
+ favicon = {
8
+ image: null,
9
+ type: 'image/png'
10
+ };
2
11
  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
12
  this.title = title;
12
13
  }
13
14
  setTitle(title) {