structured-fw 1.5.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.
package/README.md CHANGED
@@ -134,13 +134,14 @@ new Application(config);
134
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
135
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)
136
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
137
138
  - `afterRoutes` - runs after all routes are loaded from `StructuredConfig.routes.path`. Callback receives no arguments
138
139
  - `beforeComponentsLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
139
140
  - `afterComponentsLoaded` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives instance of Components as the first argument
140
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(...)`
141
142
  - `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
142
143
  - `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
143
- - `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
144
145
  - **Callback to any of the `ApplicationEvents` is expected to be an async function**
145
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:
146
147
  - numeric values -> `number`
@@ -322,9 +323,10 @@ class RequestContext<Body extends LooseObject | undefined = LooseObject> = {
322
323
  // redirect to given URI, with given statusCode (default 302)
323
324
  redirect: (to: string, statusCode?: number) => void;
324
325
 
325
- // show a 404 page and send a 404 status code
326
- // by default it will be a page with content "Page not found"
327
- // to change this you can set app.request.pageNotFoundCallback to a function that returns a Document
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'); })
328
330
  show404: () => Promise<void>
329
331
  }
330
332
  ```
@@ -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;
@@ -25,7 +25,7 @@ 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;
@@ -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);
@@ -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) {
@@ -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) {
@@ -1,132 +1,130 @@
1
1
  export class FormValidation {
2
- constructor() {
3
- this.fieldRules = [];
4
- this.singleError = false;
5
- this.customValidators = [];
6
- this.validators = {
7
- 'required': async (data, field) => {
8
- if (!(field in data)) {
9
- return false;
10
- }
11
- const value = data[field];
12
- if (value === null ||
13
- value === undefined ||
14
- (typeof value === 'string' && value.trim().length === 0)) {
15
- return false;
16
- }
2
+ fieldRules = [];
3
+ singleError = false;
4
+ customValidators = [];
5
+ validators = {
6
+ 'required': async (data, field) => {
7
+ if (!(field in data)) {
8
+ return false;
9
+ }
10
+ const value = data[field];
11
+ if (value === null ||
12
+ value === undefined ||
13
+ (typeof value === 'string' && value.trim().length === 0)) {
14
+ return false;
15
+ }
16
+ return true;
17
+ },
18
+ 'number': async (data, field) => {
19
+ const value = data[field];
20
+ if (typeof value === 'number') {
17
21
  return true;
18
- },
19
- 'number': async (data, field) => {
20
- const value = data[field];
21
- if (typeof value === 'number') {
22
- return true;
23
- }
24
- if (typeof value !== 'string') {
25
- return false;
26
- }
27
- return /^-?\d+$/.test(value);
28
- },
29
- 'float': async (data, field) => {
30
- const value = data[field];
31
- if (typeof value === 'number') {
32
- return true;
33
- }
34
- if (typeof value !== 'string') {
35
- return false;
36
- }
37
- return /^-?\d+\.\d+$/.test(value);
38
- },
39
- 'numeric': async (data, field, arg, rules) => {
40
- return await this.validators['number'](data, field, arg, rules) || await this.validators['float'](data, field, arg, rules);
41
- },
42
- 'min': async (data, field, arg, rules) => {
43
- const value = data[field];
44
- if (typeof value === 'number') {
45
- return value >= arg;
46
- }
47
- if (typeof value !== 'string') {
48
- return false;
49
- }
50
- if (await this.validators['numeric'](data, field, arg, rules)) {
51
- return parseFloat(value) >= arg;
52
- }
22
+ }
23
+ if (typeof value !== 'string') {
53
24
  return false;
54
- },
55
- 'max': async (data, field, arg, rules) => {
56
- const value = data[field];
57
- if (typeof value === 'number') {
58
- return value <= arg;
59
- }
60
- if (typeof value !== 'string') {
61
- return false;
62
- }
63
- if (await this.validators['numeric'](data, field, arg, rules)) {
64
- return parseFloat(value) <= arg;
65
- }
25
+ }
26
+ return /^-?\d+$/.test(value);
27
+ },
28
+ 'float': async (data, field) => {
29
+ const value = data[field];
30
+ if (typeof value === 'number') {
31
+ return true;
32
+ }
33
+ if (typeof value !== 'string') {
66
34
  return false;
67
- },
68
- 'minLength': async (data, field, arg) => {
69
- const value = data[field];
70
- if (typeof value !== 'string') {
71
- return false;
72
- }
73
- return value.length >= arg;
74
- },
75
- 'maxLength': async (data, field, arg) => {
76
- const value = data[field];
77
- if (typeof value !== 'string') {
78
- return false;
79
- }
80
- return value.length <= arg;
81
- },
82
- 'alphanumeric': async (data, field) => {
83
- const value = data[field];
84
- if (typeof value !== 'string') {
85
- return false;
86
- }
87
- return /^[a-zA-Z0-9]+$/.test(value);
88
- },
89
- 'validEmail': async (data, field) => {
90
- const value = data[field];
91
- if (typeof value !== 'string') {
92
- return false;
93
- }
94
- return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(value);
95
35
  }
96
- };
97
- this.decorators = {
98
- 'required': (fieldHuman) => {
99
- return `${fieldHuman} is required`;
100
- },
101
- 'number': (fieldHuman) => {
102
- return `${fieldHuman} has to be a whole number`;
103
- },
104
- 'float': (fieldHuman) => {
105
- return `${fieldHuman} has to be a decimal number`;
106
- },
107
- 'numeric': (fieldHuman) => {
108
- return `${fieldHuman} has to contain a numeric value`;
109
- },
110
- 'min': (fieldHuman, data, field, arg) => {
111
- return `${fieldHuman} has to be a value greater than ${arg}`;
112
- },
113
- 'max': (fieldHuman, data, field, arg) => {
114
- return `${fieldHuman} has to be a value lower than ${arg}`;
115
- },
116
- 'minLength': (fieldHuman, data, field, arg) => {
117
- return `${fieldHuman} has to contain at least ${arg} characters`;
118
- },
119
- 'maxLength': (fieldHuman, data, field, arg) => {
120
- return `${fieldHuman} has to contain no more than ${arg} characters`;
121
- },
122
- 'alphanumeric': (fieldHuman) => {
123
- return `${fieldHuman} can contain only letter and numbers`;
124
- },
125
- 'validEmail': () => {
126
- return `Please enter a valid email address`;
36
+ return /^-?\d+\.\d+$/.test(value);
37
+ },
38
+ 'numeric': async (data, field, arg, rules) => {
39
+ return await this.validators['number'](data, field, arg, rules) || await this.validators['float'](data, field, arg, rules);
40
+ },
41
+ 'min': async (data, field, arg, rules) => {
42
+ const value = data[field];
43
+ if (typeof value === 'number') {
44
+ return value >= arg;
127
45
  }
128
- };
129
- }
46
+ if (typeof value !== 'string') {
47
+ return false;
48
+ }
49
+ if (await this.validators['numeric'](data, field, arg, rules)) {
50
+ return parseFloat(value) >= arg;
51
+ }
52
+ return false;
53
+ },
54
+ 'max': async (data, field, arg, rules) => {
55
+ const value = data[field];
56
+ if (typeof value === 'number') {
57
+ return value <= arg;
58
+ }
59
+ if (typeof value !== 'string') {
60
+ return false;
61
+ }
62
+ if (await this.validators['numeric'](data, field, arg, rules)) {
63
+ return parseFloat(value) <= arg;
64
+ }
65
+ return false;
66
+ },
67
+ 'minLength': async (data, field, arg) => {
68
+ const value = data[field];
69
+ if (typeof value !== 'string') {
70
+ return false;
71
+ }
72
+ return value.length >= arg;
73
+ },
74
+ 'maxLength': async (data, field, arg) => {
75
+ const value = data[field];
76
+ if (typeof value !== 'string') {
77
+ return false;
78
+ }
79
+ return value.length <= arg;
80
+ },
81
+ 'alphanumeric': async (data, field) => {
82
+ const value = data[field];
83
+ if (typeof value !== 'string') {
84
+ return false;
85
+ }
86
+ return /^[a-zA-Z0-9]+$/.test(value);
87
+ },
88
+ 'validEmail': async (data, field) => {
89
+ const value = data[field];
90
+ if (typeof value !== 'string') {
91
+ return false;
92
+ }
93
+ return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(value);
94
+ }
95
+ };
96
+ decorators = {
97
+ 'required': (fieldHuman) => {
98
+ return `${fieldHuman} is required`;
99
+ },
100
+ 'number': (fieldHuman) => {
101
+ return `${fieldHuman} has to be a whole number`;
102
+ },
103
+ 'float': (fieldHuman) => {
104
+ return `${fieldHuman} has to be a decimal number`;
105
+ },
106
+ 'numeric': (fieldHuman) => {
107
+ return `${fieldHuman} has to contain a numeric value`;
108
+ },
109
+ 'min': (fieldHuman, data, field, arg) => {
110
+ return `${fieldHuman} has to be a value greater than ${arg}`;
111
+ },
112
+ 'max': (fieldHuman, data, field, arg) => {
113
+ return `${fieldHuman} has to be a value lower than ${arg}`;
114
+ },
115
+ 'minLength': (fieldHuman, data, field, arg) => {
116
+ return `${fieldHuman} has to contain at least ${arg} characters`;
117
+ },
118
+ 'maxLength': (fieldHuman, data, field, arg) => {
119
+ return `${fieldHuman} has to contain no more than ${arg} characters`;
120
+ },
121
+ 'alphanumeric': (fieldHuman) => {
122
+ return `${fieldHuman} can contain only letter and numbers`;
123
+ },
124
+ 'validEmail': () => {
125
+ return `Please enter a valid email address`;
126
+ }
127
+ };
130
128
  addRule(fieldName, nameHumanReadable, rules) {
131
129
  const rule = {
132
130
  field: [fieldName, nameHumanReadable],
@@ -1,9 +1,7 @@
1
1
  import { default as HandlebarsInstance } from 'handlebars';
2
2
  export class Handlebars {
3
- constructor() {
4
- this.instance = HandlebarsInstance;
5
- this.helpers = {};
6
- }
3
+ instance = HandlebarsInstance;
4
+ helpers = {};
7
5
  register(name, helper) {
8
6
  this.helpers[name] = helper;
9
7
  this.instance.registerHelper(name, helper);
@@ -1,6 +1,10 @@
1
1
  import { Component } from "./Component.js";
2
2
  import { Document } from "./Document.js";
3
3
  export class Layout {
4
+ layoutComponent;
5
+ app;
6
+ language;
7
+ attributes;
4
8
  constructor(app, layoutComponent, language = 'en', attributes) {
5
9
  this.app = app;
6
10
  this.layoutComponent = layoutComponent;
@@ -2,10 +2,8 @@ import { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { LooseObject } from '../types/general.types.js';
3
3
  import { RequestMethod, RequestCallback } from "../types/request.types.js";
4
4
  import { Application } from "./Application.js";
5
- import { Document } from "./Document.js";
6
5
  export declare class Request {
7
6
  private app;
8
- pageNotFoundCallback: RequestCallback<void | Document, LooseObject | undefined>;
9
7
  constructor(app: Application);
10
8
  private readonly handlers;
11
9
  on<R extends any, Body extends LooseObject | undefined = LooseObject>(methods: RequestMethod | Array<RequestMethod>, pattern: string | RegExp | Array<string | RegExp>, callback: RequestCallback<R, Body>, scope?: any, isStaticAsset?: boolean): void;
@@ -1,16 +1,13 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { RequestContext } from "./RequestContext.js";
4
+ import { StructuredError } from "../StructuredError.js";
4
5
  export class Request {
6
+ app;
5
7
  constructor(app) {
6
- this.pageNotFoundCallback = async ({ response }) => {
7
- response.statusCode = 404;
8
- response.write('Page not found');
9
- response.end();
10
- };
11
- this.handlers = [];
12
8
  this.app = app;
13
9
  }
10
+ handlers = [];
14
11
  on(methods, pattern, callback, scope, isStaticAsset = false) {
15
12
  if (!(methods instanceof Array)) {
16
13
  methods = [methods];
@@ -92,7 +89,18 @@ export class Request {
92
89
  uri = uriParts[0];
93
90
  }
94
91
  const handler = this.getHandler(uri, requestMethod);
95
- new RequestContext(this.app, request, response, handler, this.pageNotFoundCallback);
92
+ try {
93
+ const ctx = new RequestContext(this.app, request, response, handler);
94
+ await ctx.exec();
95
+ }
96
+ catch (e) {
97
+ if (e instanceof StructuredError) {
98
+ e.log();
99
+ }
100
+ else {
101
+ throw e;
102
+ }
103
+ }
96
104
  }
97
105
  patternToSegments(pattern) {
98
106
  const segments = [];
@@ -1,12 +1,13 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
- import { LooseObject, PostedDataDecoded, RequestBodyRecordValue, RequestCallback, RequestHandler, URIArguments } from "../Types.js";
2
+ import { LooseObject, PostedDataDecoded, RequestBodyRecordValue, RequestHandler, URIArguments } from "../Types.js";
3
3
  import { Application } from "./Application.js";
4
4
  import { Document } from "./Document.js";
5
5
  import { Layout } from "./Layout.js";
6
6
  export declare class RequestContext<Body extends LooseObject | undefined = LooseObject> {
7
+ private executionStartedAt;
8
+ private executionCompletedAt;
7
9
  readonly app: Application;
8
10
  uri: string;
9
- private readonly pageNotFoundCallback;
10
11
  private readonly handler;
11
12
  readonly request: IncomingMessage;
12
13
  readonly response: ServerResponse;
@@ -20,8 +21,8 @@ export declare class RequestContext<Body extends LooseObject | undefined = Loose
20
21
  getArgs: PostedDataDecoded;
21
22
  readonly timeStart: number;
22
23
  private streamingData;
23
- constructor(app: Application, request: IncomingMessage, response: ServerResponse, handler: RequestHandler | null, pageNotFoundCallback: RequestCallback<void | Document, LooseObject | undefined>);
24
- private exec;
24
+ constructor(app: Application, request: IncomingMessage, response: ServerResponse, handler: RequestHandler | null);
25
+ exec(): Promise<void>;
25
26
  respondWith(data: any): Promise<void>;
26
27
  private sendResponse;
27
28
  createDocument(title: string, component: string, data?: LooseObject): Promise<Document>;
@@ -37,5 +38,6 @@ export declare class RequestContext<Body extends LooseObject | undefined = Loose
37
38
  private extractURIArguments;
38
39
  private handle;
39
40
  isAjax(): boolean;
40
- private error;
41
+ duration(): number;
42
+ complete(): boolean;
41
43
  }
@@ -3,13 +3,26 @@ import { mergeDeep, queryStringDecode, queryStringDecodedSetValue } from "../Uti
3
3
  import { Document } from "./Document.js";
4
4
  import path from "node:path";
5
5
  import { existsSync, readFileSync, ReadStream } from "node:fs";
6
+ import { StructuredError } from "../StructuredError.js";
6
7
  export class RequestContext {
7
- constructor(app, request, response, handler, pageNotFoundCallback) {
8
- this.args = {};
9
- this.cookies = {};
10
- this.data = {};
11
- this.getArgs = {};
12
- this.streamingData = false;
8
+ executionStartedAt = null;
9
+ executionCompletedAt = null;
10
+ app;
11
+ uri;
12
+ handler;
13
+ request;
14
+ response;
15
+ args = {};
16
+ cookies = {};
17
+ body;
18
+ bodyRaw;
19
+ files;
20
+ data = {};
21
+ sessionId;
22
+ getArgs = {};
23
+ timeStart;
24
+ streamingData = false;
25
+ constructor(app, request, response, handler) {
13
26
  this.timeStart = Date.now();
14
27
  this.uri = request.url || '/';
15
28
  this.app = app;
@@ -17,21 +30,31 @@ export class RequestContext {
17
30
  this.response = response;
18
31
  this.handler = handler;
19
32
  this.body = undefined;
20
- this.pageNotFoundCallback = pageNotFoundCallback;
21
- this.exec();
22
33
  }
23
34
  async exec() {
24
- this.initGetArgs();
25
- this.parseCookies();
26
- if (this.handler) {
27
- try {
35
+ if (this.executionStartedAt !== null) {
36
+ return;
37
+ }
38
+ this.executionStartedAt = Date.now();
39
+ try {
40
+ this.initGetArgs();
41
+ this.parseCookies();
42
+ if (this.handler) {
28
43
  await this.parseBody();
29
44
  }
30
- catch (e) {
31
- this.error(e);
45
+ await this.handle();
46
+ this.executionCompletedAt = Date.now();
47
+ }
48
+ catch (e) {
49
+ const res = await this.app.emit('requestHandleError', this);
50
+ if (res.length > 0) {
51
+ if (!!res[0]) {
52
+ await this.respondWith(res[0]);
53
+ }
32
54
  }
55
+ this.response.end();
56
+ throw new StructuredError(`Error in request to ${this.uri}`, e);
33
57
  }
34
- await this.handle();
35
58
  }
36
59
  async respondWith(data) {
37
60
  if (typeof data === 'string' || Buffer.isBuffer(data)) {
@@ -91,11 +114,10 @@ export class RequestContext {
91
114
  return await layout.document(this, title, component, data, attributes);
92
115
  }
93
116
  async show404() {
94
- this.app.emit('pageNotFound', this);
95
117
  this.response.statusCode = 404;
96
- const res = await this.pageNotFoundCallback.apply(this.app, [this]);
97
- if (res instanceof Document) {
98
- await this.respondWith(res);
118
+ const res = await this.app.emit('pageNotFound', this);
119
+ if (res.length > 0 && !!res[0]) {
120
+ await this.respondWith(res[0]);
99
121
  }
100
122
  }
101
123
  initGetArgs() {
@@ -139,11 +161,7 @@ export class RequestContext {
139
161
  this.body = queryStringDecode(bodyRaw);
140
162
  }
141
163
  catch (e) {
142
- console.error(`Error parsing urlencoded request body for request to ${this.request.url}, (${e.message}).
143
- Raw data:
144
- ${bodyRaw}
145
-
146
- `);
164
+ throw new StructuredError(`Error parsing urlencoded request body, raw data: ${bodyRaw}`, e);
147
165
  }
148
166
  }
149
167
  else if (this.request.headers['content-type'].indexOf('multipart/form-data') > -1) {
@@ -154,21 +172,13 @@ export class RequestContext {
154
172
  this.body = this.parseBodyMultipart(this.bodyRaw.toString('utf-8'), boundary);
155
173
  }
156
174
  catch (e) {
157
- console.error(`Error parsing multipart request body for request to ${this.request.url}, (${e.message}).
158
- Raw data:
159
- ${this.bodyRaw.toString('utf-8')}
160
-
161
- `);
175
+ throw new StructuredError(`Error parsing multipart request body, raw data: ${this.bodyRaw.toString('utf-8')}`, e);
162
176
  }
163
177
  try {
164
178
  this.files = this.multipartBodyFiles(this.bodyRaw.toString('binary'), boundary);
165
179
  }
166
180
  catch (e) {
167
- this.error(`Error parsing multipart request body files: (${e.message}).
168
- Raw data:
169
- ${this.bodyRaw.toString('utf-8')}
170
-
171
- `);
181
+ throw new StructuredError(`Error parsing multipart request body files, raw data ${this.bodyRaw.toString('utf-8')}`, e);
172
182
  }
173
183
  }
174
184
  }
@@ -177,7 +187,7 @@ export class RequestContext {
177
187
  this.body = JSON.parse(this.bodyRaw.toString());
178
188
  }
179
189
  catch (e) {
180
- this.body = undefined;
190
+ throw new StructuredError(`Error parsing JSON request body, raw data: ${this.bodyRaw.toString('utf-8')}`, e);
181
191
  }
182
192
  }
183
193
  }
@@ -263,7 +273,7 @@ export class RequestContext {
263
273
  }
264
274
  }
265
275
  catch (e) {
266
- console.log('Error executing request handler ', e, this.handler.callback.toString());
276
+ throw new StructuredError(`Error executing request handler ${this.handler.callback.name}`, e);
267
277
  }
268
278
  if (!this.handler.staticAsset) {
269
279
  await this.app.emit('afterRequestHandler', this);
@@ -300,8 +310,13 @@ export class RequestContext {
300
310
  isAjax() {
301
311
  return this.request.headers['x-requested-with'] == 'xmlhttprequest';
302
312
  }
303
- error(e) {
304
- const message = typeof e === 'string' ? e : e.message;
305
- console.error(`Error in request to ${this.uri}: ${message}`);
313
+ duration() {
314
+ if (this.executionStartedAt === null || this.executionCompletedAt === null) {
315
+ return 0;
316
+ }
317
+ return this.executionCompletedAt - this.executionStartedAt;
318
+ }
319
+ complete() {
320
+ return this.executionCompletedAt !== null;
306
321
  }
307
322
  }
@@ -1,8 +1,9 @@
1
1
  import { randomString } from '../Util.js';
2
2
  export class Session {
3
+ application;
4
+ enabled = false;
5
+ sessions = {};
3
6
  constructor(app) {
4
- this.enabled = false;
5
- this.sessions = {};
6
7
  this.application = app;
7
8
  this.application.on('beforeRequestHandler', async (ctx) => {
8
9
  if (this.enabled) {
@@ -7,14 +7,18 @@ export const recognizedHTMLTags = [
7
7
  'svg', 'g', 'text', 'path', 'circle', 'clipPath', 'defs', 'ellipse', 'rect', 'polygon', 'image', 'style',
8
8
  ];
9
9
  export class DOMNode {
10
+ tagName;
11
+ root;
12
+ parentNode = null;
13
+ children = [];
14
+ isRoot;
15
+ attributes = [];
16
+ attributeMap = {};
17
+ style = {};
18
+ selfClosing;
19
+ explicitSelfClosing = false;
20
+ potentialComponentChildren = [];
10
21
  constructor(root, parentNode, tagName) {
11
- this.parentNode = null;
12
- this.children = [];
13
- this.attributes = [];
14
- this.attributeMap = {};
15
- this.style = {};
16
- this.explicitSelfClosing = false;
17
- this.potentialComponentChildren = [];
18
22
  this.root = root === null ? this : root;
19
23
  this.isRoot = root === null;
20
24
  this.parentNode = parentNode;
@@ -1,15 +1,17 @@
1
1
  import { DOMFragment } from "./DOMFragment.js";
2
2
  import { DOMNode } from "./DOMNode.js";
3
3
  export class HTMLParser {
4
+ html;
5
+ offset = 0;
6
+ context;
7
+ state = 'idle';
8
+ tokenCurrent = '';
9
+ fragment = new DOMFragment();
10
+ explicitSelfClosing = false;
11
+ attributeOpenQuote = '"';
12
+ attributeNameCurrent = '';
13
+ attributeContext = null;
4
14
  constructor(html) {
5
- this.offset = 0;
6
- this.state = 'idle';
7
- this.tokenCurrent = '';
8
- this.fragment = new DOMFragment();
9
- this.explicitSelfClosing = false;
10
- this.attributeOpenQuote = '"';
11
- this.attributeNameCurrent = '';
12
- this.attributeContext = null;
13
15
  this.html = html;
14
16
  this.context = this.fragment;
15
17
  while (this.parse()) {
@@ -1 +1 @@
1
- export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
1
+ export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'requestHandleError' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "type": "module",
21
21
  "main": "build/index",
22
- "version": "1.5.0",
22
+ "version": "1.6.0",
23
23
  "scripts": {
24
24
  "develop": "tsc --watch",
25
25
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,hbs,css index.js",