structured-fw 1.5.0 → 1.7.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/Config.ts CHANGED
@@ -15,11 +15,11 @@ export const config: StructuredConfig = {
15
15
  // setting this to false disallows the use of ClientComponent.redraw and ClientComponent.add
16
16
  componentRender: '/componentRender',
17
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
- }
18
+ // array of glob patterns that should be treated as static assets
19
+ // make sure to only include files you want to be publicly accessible
20
+ staticAssets: [
21
+ '/assets/**/*'
22
+ ]
23
23
  },
24
24
  routes: {
25
25
  path: '/app/routes'
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,7 @@ 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;
43
+ isAsset(): boolean;
41
44
  }
@@ -3,13 +3,27 @@ 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";
7
+ import { minimatch } from "minimatch";
6
8
  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;
9
+ executionStartedAt = null;
10
+ executionCompletedAt = null;
11
+ app;
12
+ uri;
13
+ handler;
14
+ request;
15
+ response;
16
+ args = {};
17
+ cookies = {};
18
+ body;
19
+ bodyRaw;
20
+ files;
21
+ data = {};
22
+ sessionId;
23
+ getArgs = {};
24
+ timeStart;
25
+ streamingData = false;
26
+ constructor(app, request, response, handler) {
13
27
  this.timeStart = Date.now();
14
28
  this.uri = request.url || '/';
15
29
  this.app = app;
@@ -17,21 +31,31 @@ export class RequestContext {
17
31
  this.response = response;
18
32
  this.handler = handler;
19
33
  this.body = undefined;
20
- this.pageNotFoundCallback = pageNotFoundCallback;
21
- this.exec();
22
34
  }
23
35
  async exec() {
24
- this.initGetArgs();
25
- this.parseCookies();
26
- if (this.handler) {
27
- try {
36
+ if (this.executionStartedAt !== null) {
37
+ return;
38
+ }
39
+ this.executionStartedAt = Date.now();
40
+ try {
41
+ this.initGetArgs();
42
+ this.parseCookies();
43
+ if (this.handler) {
28
44
  await this.parseBody();
29
45
  }
30
- catch (e) {
31
- this.error(e);
46
+ await this.handle();
47
+ this.executionCompletedAt = Date.now();
48
+ }
49
+ catch (e) {
50
+ const res = await this.app.emit('requestHandleError', this);
51
+ if (res.length > 0) {
52
+ if (!!res[0]) {
53
+ await this.respondWith(res[0]);
54
+ }
32
55
  }
56
+ this.response.end();
57
+ throw new StructuredError(`Error in request to ${this.uri}`, e);
33
58
  }
34
- await this.handle();
35
59
  }
36
60
  async respondWith(data) {
37
61
  if (typeof data === 'string' || Buffer.isBuffer(data)) {
@@ -91,11 +115,10 @@ export class RequestContext {
91
115
  return await layout.document(this, title, component, data, attributes);
92
116
  }
93
117
  async show404() {
94
- this.app.emit('pageNotFound', this);
95
118
  this.response.statusCode = 404;
96
- const res = await this.pageNotFoundCallback.apply(this.app, [this]);
97
- if (res instanceof Document) {
98
- await this.respondWith(res);
119
+ const res = await this.app.emit('pageNotFound', this);
120
+ if (res.length > 0 && !!res[0]) {
121
+ await this.respondWith(res[0]);
99
122
  }
100
123
  }
101
124
  initGetArgs() {
@@ -139,11 +162,7 @@ export class RequestContext {
139
162
  this.body = queryStringDecode(bodyRaw);
140
163
  }
141
164
  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
- `);
165
+ throw new StructuredError(`Error parsing urlencoded request body, raw data: ${bodyRaw}`, e);
147
166
  }
148
167
  }
149
168
  else if (this.request.headers['content-type'].indexOf('multipart/form-data') > -1) {
@@ -154,21 +173,13 @@ export class RequestContext {
154
173
  this.body = this.parseBodyMultipart(this.bodyRaw.toString('utf-8'), boundary);
155
174
  }
156
175
  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
- `);
176
+ throw new StructuredError(`Error parsing multipart request body, raw data: ${this.bodyRaw.toString('utf-8')}`, e);
162
177
  }
163
178
  try {
164
179
  this.files = this.multipartBodyFiles(this.bodyRaw.toString('binary'), boundary);
165
180
  }
166
181
  catch (e) {
167
- this.error(`Error parsing multipart request body files: (${e.message}).
168
- Raw data:
169
- ${this.bodyRaw.toString('utf-8')}
170
-
171
- `);
182
+ throw new StructuredError(`Error parsing multipart request body files, raw data ${this.bodyRaw.toString('utf-8')}`, e);
172
183
  }
173
184
  }
174
185
  }
@@ -177,7 +188,7 @@ export class RequestContext {
177
188
  this.body = JSON.parse(this.bodyRaw.toString());
178
189
  }
179
190
  catch (e) {
180
- this.body = undefined;
191
+ throw new StructuredError(`Error parsing JSON request body, raw data: ${this.bodyRaw.toString('utf-8')}`, e);
181
192
  }
182
193
  }
183
194
  }
@@ -263,7 +274,7 @@ export class RequestContext {
263
274
  }
264
275
  }
265
276
  catch (e) {
266
- console.log('Error executing request handler ', e, this.handler.callback.toString());
277
+ throw new StructuredError(`Error executing request handler ${this.handler.callback.name}`, e);
267
278
  }
268
279
  if (!this.handler.staticAsset) {
269
280
  await this.app.emit('afterRequestHandler', this);
@@ -271,7 +282,7 @@ export class RequestContext {
271
282
  }
272
283
  else {
273
284
  let staticAsset = false;
274
- if (this.app.config.url.isAsset(this.request.url || '')) {
285
+ if (this.isAsset()) {
275
286
  const basePath = this.request.url?.startsWith('/assets/ts/') ? './' : '../';
276
287
  const assetPath = path.resolve(basePath + this.request.url);
277
288
  if (existsSync(assetPath)) {
@@ -300,8 +311,24 @@ export class RequestContext {
300
311
  isAjax() {
301
312
  return this.request.headers['x-requested-with'] == 'xmlhttprequest';
302
313
  }
303
- error(e) {
304
- const message = typeof e === 'string' ? e : e.message;
305
- console.error(`Error in request to ${this.uri}: ${message}`);
314
+ duration() {
315
+ if (this.executionStartedAt === null || this.executionCompletedAt === null) {
316
+ return 0;
317
+ }
318
+ return this.executionCompletedAt - this.executionStartedAt;
319
+ }
320
+ complete() {
321
+ return this.executionCompletedAt !== null;
322
+ }
323
+ isAsset() {
324
+ return this.app.config.url.staticAssets.some((pattern) => {
325
+ return minimatch(this.uri, pattern, {
326
+ nonegate: true,
327
+ nobrace: true,
328
+ noext: true,
329
+ nocomment: true,
330
+ partial: false,
331
+ });
332
+ });
306
333
  }
307
334
  }
@@ -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';
@@ -4,7 +4,7 @@ export type StructuredConfig = {
4
4
  url: {
5
5
  removeTrailingSlash: boolean;
6
6
  componentRender: false | string;
7
- isAsset: (url: string) => boolean;
7
+ staticAssets: Array<string>;
8
8
  };
9
9
  routes: {
10
10
  readonly path: string;
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.7.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",
@@ -37,7 +37,8 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "handlebars": "^4.7.9",
40
- "mime-types": "^3.0.0"
40
+ "mime-types": "^3.0.0",
41
+ "minimatch": "^10.2.5"
41
42
  },
42
43
  "files": [
43
44
  "build/system/*",