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 +5 -5
- package/README.md +6 -4
- package/build/system/EventEmitter.js +4 -6
- package/build/system/StructuredError.d.ts +8 -0
- package/build/system/StructuredError.js +40 -0
- package/build/system/bin/structured.js +1 -2
- package/build/system/client/ClientApplication.js +3 -2
- package/build/system/client/ClientComponent.js +21 -13
- package/build/system/client/DataStore.js +2 -4
- package/build/system/client/DataStoreView.js +3 -1
- package/build/system/client/NetRequest.js +7 -2
- package/build/system/server/Application.d.ts +1 -1
- package/build/system/server/Application.js +12 -7
- package/build/system/server/Component.js +15 -7
- package/build/system/server/Components.js +3 -2
- package/build/system/server/Document.js +10 -7
- package/build/system/server/DocumentHead.js +9 -8
- package/build/system/server/FormValidation.js +121 -123
- package/build/system/server/Handlebars.js +2 -4
- package/build/system/server/Layout.js +4 -0
- package/build/system/server/Request.d.ts +0 -2
- package/build/system/server/Request.js +15 -7
- package/build/system/server/RequestContext.d.ts +8 -5
- package/build/system/server/RequestContext.js +67 -40
- package/build/system/server/Session.js +3 -2
- package/build/system/server/dom/DOMNode.js +11 -7
- package/build/system/server/dom/HTMLParser.js +10 -8
- package/build/system/types/application.types.d.ts +1 -1
- package/build/system/types/structured.types.d.ts +1 -1
- package/package.json +3 -2
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
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
326
|
-
//
|
|
327
|
-
//
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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": "
|
|
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
|
-
|
|
4
|
-
|
|
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,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
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
97
|
-
if (res
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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';
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "build/index",
|
|
22
|
-
"version": "1.
|
|
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/*",
|