structured-fw 0.8.1 → 0.8.3

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
@@ -8,15 +8,20 @@ It works with Node.js and Deno runtimes. Other runtimes are not tested.
8
8
  - [Why Structured](#why-structured)
9
9
  - [Audience](#audience)
10
10
  - [Getting started](#getting-started)
11
+ - [Key concepts](#key-concepts)
12
+ - [Good to know](#good-to-know)
13
+
11
14
 
12
15
  ### Key concepts:
13
16
  * [Application](#application)
14
17
  * [Route](#route)
15
18
  * [Document](#document)
16
- * [ClientComponent](#component) (component)
19
+ * [Component](#component)
17
20
 
18
21
  ## Getting started
19
22
 
23
+ _Following getting started instructions are relevant for Node.js runtime, if you are using Deno skip to [runtimes](#runtimes) section._
24
+
20
25
  ### Initialize a Node.js project
21
26
  ```
22
27
  cd /path/to/project
@@ -106,12 +111,12 @@ new Application(config);
106
111
  ### Methods
107
112
  - `init(): Promise<void>` - initializes application, you only need to run this if you set `autoInit = false` in config, otherwise this will be ran when you create the Application instance
108
113
  - `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `ApplicationEvenets`:
109
- - `serverStarted` - executed once the built-in http server is started and running. Callback receives no arguments
114
+ - `serverStarted` - executed once the built-in http server is started and running. Callback receives Server (exported from node:http) instance as the first argument
110
115
  - `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)
111
116
  - `afterRequestHandler` - runs after any request handler (route) is executed. Callback receives `RequestContext` as the first argument
112
117
  - `afterRoutes` - runs after all routes are loaded from `StructuredConfig.routes.path`. Callback receives no arguments
113
- - `beforeComponentLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
114
- - `afterComponentLoad` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
118
+ - `beforeComponentsLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
119
+ - `afterComponentsLoaded` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives instance of Components as the first argument
115
120
  - `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(...)`
116
121
  - `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
117
122
  - `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
@@ -319,7 +324,7 @@ In some edge cases you may need more control of when a route is executed, in whi
319
324
  >}>('POST', '/users/create', asyc (ctx) => {
320
325
  > ctx.body.email // string
321
326
  > ctx.body.age // number
322
- > const doc = new Document(ctx, 'User', app);
327
+ > const doc = new Document(app, 'User', ctx);
323
328
  > return doc; // error if we return anything but Document
324
329
  > });
325
330
  > ```
@@ -605,6 +610,117 @@ Methods:
605
610
  - `arrayRef<T>(refName: string): Array<T>` - get an array of HTMLElement or ClientComponent that have attribute `array:ref="[refName]"`
606
611
  - `add(appendTo: HTMLElement, componentName: string, data?: LooseObject)` - add `componentName` component to `appendTo` element, optionally passing `data` to the component when it's being rendered
607
612
 
613
+ ## Good to know
614
+ - [Uing CSS frameworks](#css-frameworks)
615
+ - [Using JS runtimes other than Node.js](#runtimes)
616
+ - [Why not JSR](#jsr)
617
+
618
+ ### CSS frameworks
619
+ We rarely write all CSS from scratch, usually we use a CSS framework to speed us up. Structured allows you to work with any CSS frameworks such as Tailwind, PostCSS or Bootstrap.
620
+
621
+ Your Tailwind configuration may look something like:
622
+ ```
623
+ /** @type {import('tailwindcss').Config} */
624
+ module.exports = {
625
+ content: ["./app/views/**/*.html", "./app/views/**/*.hbs"],
626
+ ...
627
+ }
628
+ ```
629
+
630
+ Above we just defined where all our HTML resides, which is within /app/views. That is all there is to it. From there, you can generate the CSS, for example:\
631
+ `npx tailwindcss -i ./assets/css/src/style.css -o ./assets/css/dist.css`
632
+
633
+ **Including the output CSS**\
634
+ To include the output CSS in all pages, you can add the following to `index.ts`:
635
+ ```
636
+ const app = new Application(config);
637
+
638
+ app.on('documentCreated', (doc) => {
639
+ doc.head.addCSS('/assets/css/dist.css');
640
+ });
641
+ ```
642
+
643
+ ### Runtimes
644
+ Structured is tested with Node.js and Deno. Other runtimes would likely work as well.
645
+
646
+ To use Structured with Deno, you can:
647
+ ```
648
+ cd /path/to/project
649
+ deno init
650
+ deno add npm:structured-fw
651
+ ```
652
+
653
+ With Deno, we can't use the cli to create the boilerplate, so you will need to create it yourself.
654
+ ```
655
+ mkdir app
656
+ mkdir app/views
657
+ mkdir app/routes
658
+ ```
659
+
660
+ Create `Config.ts`:
661
+ ```
662
+ import { StructuredConfig } from "structured-fw/Types";
663
+
664
+ export const config: StructuredConfig = {
665
+ // Application.importEnv will load all env variables starting with [envPrefix]_
666
+ envPrefix: 'STRUCTURED',
667
+
668
+ // whether to call Application.init when an instance of Application is created
669
+ autoInit: true,
670
+
671
+ url: {
672
+ removeTrailingSlash: true,
673
+
674
+ // if you want to enable individual component rendering set this to URI (string)
675
+ // to disable component rendering set it to false
676
+ // setting this to false disallows the use of ClientComponent.redraw and ClientComponent.add
677
+ componentRender: '/componentRender',
678
+
679
+ // function that receives the requested URL and returns boolean, if true, treat as static asset
680
+ // if there is a registered request handler that matches this same URL, it takes precedence over this
681
+ isAsset: function(uri: string) {
682
+ return uri.indexOf('/assets/') === 0;
683
+ }
684
+ },
685
+ routes: {
686
+ path: '/app/routes'
687
+ },
688
+ components : {
689
+ // relative to index.ts
690
+ path: '/app/views',
691
+
692
+ componentNameAttribute: 'structured-component'
693
+ },
694
+ session: {
695
+ cookieName: 'session',
696
+ keyLength: 24,
697
+ durationSeconds: 60 * 60,
698
+ garbageCollectIntervalSeconds: 60
699
+ },
700
+ http: {
701
+ port: 9191,
702
+ host: '0.0.0.0',
703
+ // used by Document.push, can be preload or preconnect
704
+ linkHeaderRel : 'preload'
705
+ },
706
+ runtime: 'Deno'
707
+ }
708
+ ```
709
+
710
+ Import `Config.ts` in `main.ts` and create the Application instance:
711
+ ```
712
+ import { Application } from 'structured-fw/Application';
713
+ import { config } from './Config.ts';
714
+
715
+ new Application(config);
716
+ ```
717
+
718
+ Run application using `deno main.ts`
719
+
720
+ ### JSR
721
+ It would make a lot of sense to have Structured hosted on JSR (JavaScript Registry) given Structured is a TypeScript framework, and JSR is a TypeScript-first registry, however, the issue is that Deno imposes [limitations with dynamic imports](https://docs.deno.com/deploy/api/dynamic-import/) with JSR-imported dependencies, which are required for the framework (to dynamically import your routes and components).\
722
+ This does not stop the framework from working with Deno, but for the time being, we have to stick with good old npm.
723
+
608
724
  ## Why Structured
609
725
  Framework was developed by someone who has been a web developer for almost 20 years (me), and did not like the path web development has taken.
610
726
  \
@@ -127,7 +127,7 @@ export interface ComponentScaffold {
127
127
  [key: string]: any;
128
128
  }
129
129
  export type LooseObject = Record<string, any>;
130
- export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentLoad' | 'afterComponentLoad' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
130
+ export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'componentCreated' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
131
131
  export type SessionEntry = {
132
132
  sessionId: string;
133
133
  lastRequest: number;
@@ -1,14 +1,8 @@
1
- import { InitializerFunction, LooseObject, StructuredClientConfig } from '../Types.js';
1
+ import { LooseObject } from '../Types.js';
2
2
  import { DataStoreView } from './DataStoreView.js';
3
3
  import { DataStore } from './DataStore.js';
4
4
  import { Net } from './Net.js';
5
5
  import { EventEmitter } from './EventEmitter.js';
6
- declare global {
7
- interface Window {
8
- initializers: Record<string, InitializerFunction | string>;
9
- structuredClientConfig: StructuredClientConfig;
10
- }
11
- }
12
6
  export declare class ClientComponent extends EventEmitter {
13
7
  readonly name: string;
14
8
  children: Array<ClientComponent>;
@@ -20,7 +20,7 @@ export declare class Application {
20
20
  constructor(config: StructuredConfig);
21
21
  init(): Promise<void>;
22
22
  private start;
23
- on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext : E extends 'documentCreated' ? Document : undefined) => void): void;
23
+ 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;
24
24
  emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>>;
25
25
  importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
26
26
  exportContextFields(...fields: Array<keyof RequestContextData>): void;
@@ -35,12 +35,12 @@ export class Application {
35
35
  catch (e) {
36
36
  console.error(e.message);
37
37
  }
38
- await this.emit('beforeComponentLoad');
38
+ await this.emit('beforeComponentsLoad');
39
39
  this.components.loadComponents();
40
- await this.emit('afterComponentLoad');
40
+ await this.emit('afterComponentsLoaded', this.components);
41
41
  await this.emit('beforeRoutes');
42
42
  await this.request.loadHandlers();
43
- await this.emit('afterRoutes');
43
+ await this.emit('afterRoutes', this.request);
44
44
  if (this.config.url.componentRender !== false) {
45
45
  this.request.on('POST', `${this.config.url.componentRender}`, async (ctx) => {
46
46
  const input = ctx.body;
@@ -49,16 +49,18 @@ export class Application {
49
49
  }
50
50
  this.request.on('GET', /^\/assets\/client-js/, async ({ request, response }) => {
51
51
  const uri = request.url?.substring(18);
52
- const filePath = path.resolve('./system/', uri);
52
+ if (uri.includes('..')) {
53
+ return '';
54
+ }
55
+ const filePath = path.resolve(import.meta.dirname, '..', uri);
53
56
  if (existsSync(filePath)) {
54
57
  response.setHeader('Content-Type', 'application/javascript');
55
- response.write(readFileSync(filePath));
56
- response.end();
58
+ return readFileSync(filePath);
57
59
  }
58
60
  else {
59
61
  response.statusCode = 404;
60
62
  }
61
- return;
63
+ return '';
62
64
  }, this, true);
63
65
  await this.start();
64
66
  }
@@ -69,7 +71,7 @@ export class Application {
69
71
  });
70
72
  this.server.listen(this.config.http.port, this.config.http.host || '127.0.0.1', async () => {
71
73
  const address = (this.config.http.host !== undefined ? this.config.http.host : '') + ':' + this.config.http.port;
72
- await this.emit('serverStarted');
74
+ await this.emit('serverStarted', this.server);
73
75
  console.log(`Server started on ${address}`);
74
76
  resolve();
75
77
  });
@@ -8,6 +8,7 @@ export class Component {
8
8
  this.attributesRaw = {};
9
9
  this.attributes = {};
10
10
  this.data = {};
11
+ const isDocument = this instanceof Document;
11
12
  this.name = name;
12
13
  if (name === 'root') {
13
14
  this.dom = new DOMFragment();
@@ -21,7 +22,7 @@ export class Component {
21
22
  }
22
23
  this.isRoot = false;
23
24
  }
24
- if (this instanceof Document) {
25
+ if (isDocument) {
25
26
  this.document = this;
26
27
  }
27
28
  else {
@@ -42,6 +43,9 @@ export class Component {
42
43
  else {
43
44
  this.entry = null;
44
45
  }
46
+ if (!isDocument) {
47
+ this.document.application.emit('componentCreated', this);
48
+ }
45
49
  }
46
50
  async init(html, data) {
47
51
  this.initAttributesData();
package/index.ts CHANGED
@@ -1,4 +1,15 @@
1
1
  import { Application } from "structured-fw/Application";
2
2
  import { config } from './Config.js';
3
3
 
4
- new Application(config);
4
+ new Application(config);
5
+
6
+ // app.on('afterComponentsLoaded', (components) => {
7
+ // components.componentNames.forEach((componentName) => {
8
+ // console.log(componentName)
9
+ // console.log(components.getByName(componentName));
10
+ // });
11
+ // })
12
+
13
+ // app.on('componentCreated', (component) => {
14
+ // console.log(component.document.id);
15
+ // })
package/jsr.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@structured/structured-fw",
3
+ "version": "0.8.21",
4
+ "license": "MIT",
5
+ "exports" : {
6
+ "./Types": "./system/Types.ts",
7
+ "./Symbols": "./system/Symbols.ts",
8
+ "./Util": "./system/Util.ts",
9
+ "./Application": "./system/server/Application.ts",
10
+ "./Document": "./system/server/Document.ts",
11
+ "./FormValidation": "./system/server/FormValidation.ts",
12
+ "./ClientComponent": "./system/client/ClientComponent.ts"
13
+ },
14
+ "dependencies": {
15
+ "handlebars": "^4.7.8",
16
+ "mime-types": "^3.0.0",
17
+ "ts-md5": "^1.3.1",
18
+ "@types/node": "^22.9.3",
19
+ "@types/mime-types": "^2.1.4"
20
+ },
21
+ "exclude": [
22
+ "build",
23
+ "assets",
24
+ "node_modules",
25
+ "tsconfig.json",
26
+ "package.json",
27
+ "package.lock.json",
28
+ "tailwind.config.cjs",
29
+ ".vscode",
30
+ "app/Types.ts",
31
+ "app/views/*",
32
+ "app/routes/*",
33
+ "app/models/*"
34
+ ]
35
+ }
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "main": "build/index",
17
- "version": "0.8.1",
17
+ "version": "0.8.3",
18
18
  "scripts": {
19
19
  "develop": "tsc --watch",
20
20
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
package/system/Types.ts CHANGED
@@ -177,7 +177,7 @@ export interface ComponentScaffold {
177
177
 
178
178
  export type LooseObject = Record<string, any>
179
179
 
180
- export type ApplicationEvents = 'serverStarted'|'beforeRequestHandler'|'afterRequestHandler'|'beforeRoutes'|'afterRoutes'|'beforeComponentLoad'|'afterComponentLoad'|'documentCreated'|'beforeAssetAccess'|'afterAssetAccess'|'pageNotFound';
180
+ export type ApplicationEvents = 'serverStarted'|'beforeRequestHandler'|'afterRequestHandler'|'beforeRoutes'|'afterRoutes'|'beforeComponentsLoad'|'afterComponentsLoaded'|'componentCreated'|'documentCreated'|'beforeAssetAccess'|'afterAssetAccess'|'pageNotFound';
181
181
 
182
182
  export type SessionEntry = {
183
183
  sessionId : string,
@@ -1,4 +1,4 @@
1
- import { ClientComponentTransition, ClientComponentTransitions, InitializerFunction, LooseObject, StoreChangeCallback, StructuredClientConfig } from '../Types.js';
1
+ import { ClientComponentTransition, ClientComponentTransitions, InitializerFunction, LooseObject, StoreChangeCallback } from '../Types.js';
2
2
  import { attributeValueFromString, attributeValueToString, mergeDeep, objectEach, queryStringDecodedSetValue, toCamelCase } from '../Util.js';
3
3
  import { DataStoreView } from './DataStoreView.js';
4
4
  import { DataStore } from './DataStore.js';
@@ -6,16 +6,6 @@ import { Net } from './Net.js';
6
6
  import { NetRequest } from './NetRequest.js';
7
7
  import { EventEmitter } from './EventEmitter.js';
8
8
 
9
- // window.initializers will always be present
10
- // each Document has a list of initializers used in components within it
11
- // and they will be output as initializers = { componentName : initializer }
12
- declare global {
13
- interface Window {
14
- initializers: Record<string, InitializerFunction | string>;
15
- structuredClientConfig: StructuredClientConfig;
16
- }
17
- }
18
-
19
9
  export class ClientComponent extends EventEmitter {
20
10
  readonly name: string;
21
11
  children: Array<ClientComponent> = [];
@@ -0,0 +1,12 @@
1
+ import { InitializerFunction, StructuredClientConfig } from "./Types.js";
2
+
3
+ export {}
4
+ // window.initializers will always be present
5
+ // each Document has a list of initializers used in components within it
6
+ // and they will be output as initializers = { componentName : initializer }
7
+ declare global {
8
+ interface Window {
9
+ initializers: Record<string, InitializerFunction | string>;
10
+ structuredClientConfig: StructuredClientConfig;
11
+ }
12
+ }
@@ -59,14 +59,14 @@ export class Application {
59
59
  console.error(e.message);
60
60
  }
61
61
 
62
- await this.emit('beforeComponentLoad');
62
+ await this.emit('beforeComponentsLoad');
63
63
  this.components.loadComponents();
64
- await this.emit('afterComponentLoad');
64
+ await this.emit('afterComponentsLoaded', this.components);
65
65
 
66
66
 
67
67
  await this.emit('beforeRoutes');
68
68
  await this.request.loadHandlers();
69
- await this.emit('afterRoutes');
69
+ await this.emit('afterRoutes', this.request);
70
70
 
71
71
  if (this.config.url.componentRender !== false) {
72
72
  // special request handler, executed when ClientComponent.redraw is called
@@ -84,15 +84,15 @@ export class Application {
84
84
  // special request handler, serve the client side JS
85
85
  this.request.on('GET', /^\/assets\/client-js/, async ({ request, response }) => {
86
86
  const uri = request.url?.substring(18) as string;
87
- const filePath = path.resolve('./system/', uri);
87
+ if (uri.includes('..')) {return '';} // disallow having ".." in the URL
88
+ const filePath = path.resolve(import.meta.dirname, '..', uri);
88
89
  if (existsSync(filePath)) {
89
90
  response.setHeader('Content-Type', 'application/javascript');
90
- response.write(readFileSync(filePath));
91
- response.end();
91
+ return readFileSync(filePath);
92
92
  } else {
93
93
  response.statusCode = 404;
94
94
  }
95
- return;
95
+ return '';
96
96
  }, this, true);
97
97
 
98
98
  await this.start();
@@ -106,7 +106,7 @@ export class Application {
106
106
  });
107
107
  this.server.listen(this.config.http.port, this.config.http.host || '127.0.0.1', async () => {
108
108
  const address = (this.config.http.host !== undefined ? this.config.http.host : '') + ':' + this.config.http.port;
109
- await this.emit('serverStarted');
109
+ await this.emit('serverStarted', this.server);
110
110
  console.log(`Server started on ${address}`);
111
111
  resolve();
112
112
  });
@@ -120,6 +120,8 @@ export class Application {
120
120
  payload:
121
121
  E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext :
122
122
  E extends 'documentCreated' ? Document :
123
+ E extends 'afterComponentsLoaded' ? Components :
124
+ E extends 'serverStarted' ? Server :
123
125
  undefined
124
126
  ) => void
125
127
  ): void {
@@ -29,6 +29,7 @@ export class Component {
29
29
  isRoot: boolean;
30
30
 
31
31
  constructor(name: string, node?: DOMNode, parent?: Document|Component, autoInit: boolean = true) {
32
+ const isDocument = this instanceof Document;
32
33
  this.name = name;
33
34
 
34
35
  if (name === 'root') {
@@ -43,7 +44,7 @@ export class Component {
43
44
  this.isRoot = false;
44
45
  }
45
46
 
46
- if (this instanceof Document) {
47
+ if (isDocument) {
47
48
  // this will only happen if an instance of Document, as it extends component
48
49
  this.document = this;
49
50
  } else {
@@ -71,6 +72,10 @@ export class Component {
71
72
  } else {
72
73
  this.entry = null;
73
74
  }
75
+
76
+ if (! isDocument) {
77
+ this.document.application.emit('componentCreated', this);
78
+ }
74
79
  }
75
80
 
76
81
  // load component's data and fill it