structured-fw 0.8.43 → 0.8.71

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
@@ -5,9 +5,6 @@ Framework allows the developer to develop self-contained components which are re
5
5
 
6
6
  It works with Node.js and Deno runtimes. Other runtimes are not tested.
7
7
 
8
- > [!NOTE]
9
- > While Structured framework is in development for a couple of years and is production tested, the npm package is introduced recently and there were still issues that came up with it in the past few versions. Since version 0.8.3 all should be functional. Please update to latest version and check for updates regularly until the npm package is as stable as the framework itself. Feel free to open issues on the github page if you have any issues with the npm package or the framework. _Structured followed versioning x.y.z where z was a single digit 0-9, but since there were a couple of versions that introduced no changes to the framework and were just npm package tweaks, I decided to make an exception to the rule and allow myself to use 2 digits for such updates._
10
-
11
8
  - [Why Structured](#why-structured)
12
9
  - [Audience](#audience)
13
10
  - [Getting started](#getting-started)
@@ -41,6 +38,17 @@ npm install @types/node
41
38
  ### Create boilerplate
42
39
  `npx structured init`
43
40
 
41
+ ### Create a test route
42
+ Create a file `/app/routes/Test.ts`:
43
+ ```
44
+ import { Application } from 'structured-fw/Application';
45
+ export default function(app: Application) {
46
+ app.request.on('GET', '/test', async()=> {
47
+ return 'Hello, World!';
48
+ });
49
+ }
50
+ ```
51
+
44
52
  ### Compile
45
53
  `tsc`\
46
54
  This will create a directory `build` (or whatever you have in tsconfig.json as compilerOptions.outputDir)
@@ -57,6 +65,8 @@ cd build
57
65
  pm2 start index.js --name="[appName]"
58
66
  ```
59
67
 
68
+ If you followed the above steps, you should be able to access `http://localhost:9191/test` in your browser and see the output `Hello, World!`.
69
+
60
70
  # Key concepts
61
71
 
62
72
  ## Application
@@ -418,7 +428,7 @@ That was the simplest possible example, let's make it more interesting by adding
418
428
  ### Component server-side code
419
429
  Create a new file `/app/views/HelloWorld/HelloWorld.ts` (server side component code):
420
430
  ```
421
- import { ComponentScaffold } from 'system/Types.js';
431
+ import { ComponentScaffold } from 'structured-fw/Types';
422
432
  export default class HelloWorld implements ComponentScaffold {
423
433
  async getData(): Promise<{
424
434
  luckyNumber: number
@@ -460,7 +470,7 @@ Let's make it even more interesting by adding some client side code to it.
460
470
  ### Component client-side code
461
471
  Create `/app/views/HelloWorld/HelloWorld.client.ts`:
462
472
  ```
463
- import { InitializerFunction } from 'system/Types.js';
473
+ import { InitializerFunction } from 'structured-fw/Types';
464
474
  export const init: InitializerFunction = async function() {
465
475
  const generateNew = this.ref<HTMLButtonElement>('newNumber');
466
476
 
@@ -509,7 +519,7 @@ Parent says your lucky number is {{number}}.
509
519
  That's it. Since `AnotherComponent` has no server side code, all data passed to it is exported to HTML, hence the `number` you passed from `HelloWorld` will be readily available for use. If AnotherComponent had a server side part, the process is a bit different, it will receive it as part of the `data`, but can choose whether to make it available to the HTML, or just make use of it and return other stuff. Let's see how that works.
510
520
  Create `/app/views/AnotherComponent/AnotherComponent.ts`:
511
521
  ```
512
- import { ComponentScaffold } from 'system/Types.js';
522
+ import { ComponentScaffold } from 'structured-fw/Types';
513
523
  export default class AnotherComponent implements ComponentScaffold {
514
524
  async getData(data: { number: number }): Promise<{
515
525
  parentSuggests: number,
@@ -542,7 +552,7 @@ What about client side? **By default, data returned by server side code is not a
542
552
 
543
553
  Let's create a client side code for `AnotherComponent` and export the `betterNumber` to it, create `/app/views/AnotherComponent/AnotherComponent.client.ts`:
544
554
  ```
545
- import { InitializerFunction } from 'system/Types.js';
555
+ import { InitializerFunction } from 'structured-fw/Types';
546
556
  export const init: InitializerFunction = async function() {
547
557
  const betterNumber = this.getData<number>('betterNumber');
548
558
 
@@ -552,7 +562,7 @@ export const init: InitializerFunction = async function() {
552
562
 
553
563
  And let's update `AnotherComponent.ts` to export `betterNumber`:
554
564
  ```
555
- import { ComponentScaffold } from 'system/Types.js';
565
+ import { ComponentScaffold } from 'structured-fw/Types';
556
566
  export default class AnotherComponent implements ComponentScaffold {
557
567
  exportFields = ['betterNumber'];
558
568
  async getData(data: { number: number }): Promise<{
@@ -574,7 +584,7 @@ This concept is wrong to start with, if we want a component to be independent, i
574
584
 
575
585
  Let's say we wanted to access the `parent` Component from `AnotherComponent`:
576
586
  ```
577
- import { InitializerFunction } from 'system/Types.js';
587
+ import { InitializerFunction } from 'structured-fw/Types';
578
588
  export const init: InitializerFunction = async function() {
579
589
  const betterNumber = this.getData<number>('betterNumber');
580
590
 
@@ -585,7 +595,7 @@ Here we accessed the `parent` and obtained it's `name`.
585
595
 
586
596
  *"But we did not send any data to the parent here"* - correct, we did not, and we won't, instead we can inform them we have some data available, or that an event they might be interested in has occurred, and if they care, so be it:
587
597
  ```
588
- import { InitializerFunction } from 'system/Types.js';
598
+ import { InitializerFunction } from 'structured-fw/Types';
589
599
  export const init: InitializerFunction = async function() {
590
600
  const betterNumber = this.getData<number>('betterNumber');
591
601
 
@@ -595,7 +605,7 @@ export const init: InitializerFunction = async function() {
595
605
 
596
606
  We emitted an `event` with `eventName` = "`truth`" and a `payload`, which in this case is a string, but can be of any type. If the parent cares about it (or for that matter, not necessarily the parent, but anyone in the component tree), they can subscribe to that event. Let's subscribe to the event from `HelloWorld` (`HelloWorld.client.ts`):
597
607
  ```
598
- import { InitializerFunction } from 'system/Types.js';
608
+ import { InitializerFunction } from 'structured-fw/Types';
599
609
  export const init: InitializerFunction = async function() {
600
610
 
601
611
  const child = this.find('AnotherComponent'); // ClientComponent | null
@@ -678,6 +688,69 @@ then in ComponentName.html:
678
688
  <div data-if="showDiv()"></div>
679
689
  ```
680
690
 
691
+ ### Layout
692
+ Prior to version 0.8.7:
693
+
694
+ 1) `/app/views/layout.html`
695
+ ```
696
+ ...
697
+ {{{layoutComponent component data attributes}}}
698
+ ...
699
+ ```
700
+ 2) `/app/routes/Test.ts`
701
+ ```
702
+ import Document from 'structured-fw/Document';
703
+
704
+ app.request.on('GET', '/test', async (ctx) => {
705
+ const doc = new Document(app, 'Title', ctx);
706
+ await doc.loadComponent('layout', {
707
+ component: 'ComponentName',
708
+ data: {
709
+ something: 123
710
+ }
711
+ });
712
+ return doc;
713
+ });
714
+ ```
715
+
716
+ Version 0.8.7 introduced the `Layout` class, which allows accomplishing the above in a nicer way:
717
+ 1) `/app/views/layout.html`
718
+ ```
719
+ ...
720
+ <template></template>
721
+ ...
722
+ ```
723
+ 2) `/index.ts` (`app` is an instance of `Application`)
724
+ ```
725
+ export const layout = new Layout(app, 'layout');
726
+ ```
727
+ 3) `/app/routes/Test.ts`
728
+ ```
729
+ import { layout } from '../../index.js';
730
+
731
+ app.request.on('GET', '/test', async (ctx) => {
732
+ return await layout.document(ctx, 'Test', 'Conditionals', {
733
+ something: 123
734
+ });
735
+ });
736
+
737
+ ```
738
+
739
+ While with the new approach there is an extra step where we create the instance(s) of `Layout`, it makes the route/template code cleaner (you will create your layout instance(s) only once, while you will likely use it in many routes, so adding an extra step is worth it).
740
+
741
+ ```
742
+ Layout.document(
743
+ ctx: RequestContext,
744
+ title: string,
745
+ componentName: string,
746
+ data?: LooseObject
747
+ ): Promise<Document>
748
+ ```
749
+ `Layout.document` the only method of Layout you will use, it creates an instance of Document, loads template component (provided as second argument to Layout constructor) into it and loads `componentName` component in place of `<template></template>` found within your template.
750
+
751
+ > [!TIP]
752
+ > You will often want to use a few different layouts in your web application. You can achieve that by creating and exporting multiple instances of Layout and use the appropriate one where you need it.
753
+
681
754
  **Basic animation/transitions**\
682
755
  If you use conditionals on any DOM node, you may also enable basic animations/transitions using following attributes:
683
756
  - Enable transition:
@@ -61,9 +61,9 @@ function createTsconfig() {
61
61
  "strictNullChecks": true,
62
62
  "strictPropertyInitialization": true,
63
63
  "strictBindCallApply": true,
64
- "moduleResolution": "bundler",
64
+ "moduleResolution": "node16",
65
65
  "outDir": "./build",
66
- "module": "ES2020",
66
+ "module": "Node16",
67
67
  "target": "ES2021",
68
68
  "allowSyntheticDefaultImports": true,
69
69
  "preserveSymlinks": true,
@@ -4,6 +4,7 @@ export declare class Net {
4
4
  request(method: RequestMethod, url: string, headers?: IncomingHttpHeaders, body?: any, responseType?: XMLHttpRequestResponseType): Promise<string>;
5
5
  get(url: string, headers?: IncomingHttpHeaders): Promise<string>;
6
6
  delete(url: string, headers?: IncomingHttpHeaders): Promise<string>;
7
+ private serializeData;
7
8
  post(url: string, data: any, headers?: IncomingHttpHeaders): Promise<string>;
8
9
  put(url: string, data: any, headers?: IncomingHttpHeaders): Promise<string>;
9
10
  getJSON<T>(url: string, headers?: IncomingHttpHeaders): Promise<T>;
@@ -10,18 +10,19 @@ export class Net {
10
10
  async delete(url, headers = {}) {
11
11
  return this.request('DELETE', url, headers);
12
12
  }
13
- async post(url, data, headers = {}) {
13
+ serializeData(data, headers) {
14
14
  if (typeof data === 'object' && !headers['content-type'] && !(data instanceof FormData)) {
15
15
  headers['content-type'] = 'application/json';
16
- data = JSON.stringify(data);
16
+ return JSON.stringify(data);
17
17
  }
18
+ return data;
19
+ }
20
+ async post(url, data, headers = {}) {
21
+ data = this.serializeData(data, headers);
18
22
  return await this.request('POST', url, headers, data);
19
23
  }
20
24
  async put(url, data, headers = {}) {
21
- if (typeof data === 'object' && !headers['content-type']) {
22
- headers['content-type'] = 'application/json';
23
- data = JSON.stringify(data);
24
- }
25
+ data = this.serializeData(data, headers);
25
26
  return this.request('PUT', url, headers, data);
26
27
  }
27
28
  async getJSON(url, headers) {
@@ -1,5 +1,5 @@
1
1
  import { Server } from 'node:http';
2
- import { ApplicationEvents, LooseObject, RequestContext, StructuredConfig } from '../Types';
2
+ import { ApplicationEvents, LooseObject, RequestContext, StructuredConfig } from '../Types.js';
3
3
  import { Document } from './Document.js';
4
4
  import { Components } from './Components.js';
5
5
  import { Session } from './Session.js';
@@ -8,6 +8,7 @@ import { Handlebars } from './Handlebars.js';
8
8
  import { Cookies } from './Cookies.js';
9
9
  export declare class Application {
10
10
  readonly config: StructuredConfig;
11
+ initialized: boolean;
11
12
  server: null | Server;
12
13
  listening: boolean;
13
14
  private readonly eventEmitter;
@@ -25,6 +26,7 @@ export declare class Application {
25
26
  importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
26
27
  exportContextFields(...fields: Array<keyof RequestContextData>): void;
27
28
  contentType(extension: string): string | false;
29
+ registerPlugin<Opt extends Readonly<LooseObject>>(callback: (app: Application, options: Opt) => void | Promise<void>, opts: NoInfer<Opt>): Promise<void>;
28
30
  private respondWithComponent;
29
31
  memoryUsage(): NodeJS.MemoryUsage;
30
32
  printMemoryUsage(): void;
@@ -12,6 +12,7 @@ import { Handlebars } from './Handlebars.js';
12
12
  import { Cookies } from './Cookies.js';
13
13
  export class Application {
14
14
  constructor(config) {
15
+ this.initialized = false;
15
16
  this.server = null;
16
17
  this.listening = false;
17
18
  this.eventEmitter = new EventEmitter();
@@ -28,6 +29,9 @@ export class Application {
28
29
  }
29
30
  }
30
31
  async init() {
32
+ if (this.initialized) {
33
+ return;
34
+ }
31
35
  this.eventEmitter.setMaxListeners(10);
32
36
  try {
33
37
  await this.handlebars.loadHelpers('../Helpers.js');
@@ -63,6 +67,7 @@ export class Application {
63
67
  return '';
64
68
  }, this, true);
65
69
  await this.start();
70
+ this.initialized = true;
66
71
  }
67
72
  start() {
68
73
  return new Promise((resolve, reject) => {
@@ -136,6 +141,12 @@ export class Application {
136
141
  contentType(extension) {
137
142
  return mime.contentType(extension);
138
143
  }
144
+ async registerPlugin(callback, opts) {
145
+ if (this.initialized) {
146
+ console.warn('Plugin registered after app is initialized, some plugin features may not work.');
147
+ }
148
+ await callback.apply(this, [this, opts]);
149
+ }
139
150
  async respondWithComponent(ctx, componentName, attributes, unwrap = true) {
140
151
  const component = this.components.getByName(componentName);
141
152
  if (component) {
@@ -1,4 +1,4 @@
1
- import { ComponentEntry, StructuredConfig } from '../Types';
1
+ import { ComponentEntry, StructuredConfig } from '../Types.js';
2
2
  import { Application } from './Application.js';
3
3
  export declare class Components {
4
4
  config: StructuredConfig;
@@ -1,4 +1,4 @@
1
- import { DocumentResource } from '../Types';
1
+ import { DocumentResource } from '../Types.js';
2
2
  export declare class DocumentHead {
3
3
  title: string;
4
4
  js: Array<DocumentResource>;
@@ -1,4 +1,4 @@
1
- import { FormValidationEntry, PostedDataDecoded, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types';
1
+ import { FormValidationEntry, PostedDataDecoded, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types.js';
2
2
  export declare class FormValidation {
3
3
  fieldRules: Array<FormValidationEntry>;
4
4
  singleError: boolean;
@@ -0,0 +1,10 @@
1
+ import { LooseObject, RequestContext } from "../Types.js";
2
+ import { Application } from "./Application.js";
3
+ import { Document } from "./Document.js";
4
+ export declare class Layout {
5
+ layoutComponent: string;
6
+ app: Application;
7
+ constructor(app: Application, layoutComponent: string);
8
+ private layoutComponentExists;
9
+ document(ctx: RequestContext, title: string, componentName: string, data?: LooseObject): Promise<Document>;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { Component } from "./Component.js";
2
+ import { Document } from "./Document.js";
3
+ export class Layout {
4
+ constructor(app, layoutComponent) {
5
+ this.app = app;
6
+ this.layoutComponent = layoutComponent;
7
+ if (this.app.initialized) {
8
+ this.layoutComponentExists();
9
+ }
10
+ else {
11
+ this.app.on('afterComponentsLoaded', () => { this.layoutComponentExists(); });
12
+ }
13
+ }
14
+ layoutComponentExists() {
15
+ if (this.app.components.getByName(this.layoutComponent) === null) {
16
+ throw new Error(`Layout component "${this.layoutComponent}" not found`);
17
+ }
18
+ }
19
+ async document(ctx, title, componentName, data) {
20
+ const doc = new Document(this.app, title, ctx);
21
+ await doc.loadComponent(this.layoutComponent, data);
22
+ const layoutComponent = doc.dom.queryByTagName('template');
23
+ if (layoutComponent.length === 0) {
24
+ throw new Error(`<template></template> not found in the layout component ${this.layoutComponent}`);
25
+ }
26
+ const component = new Component(componentName, layoutComponent[0], doc, false);
27
+ await component.init(`<${componentName}></${componentName}>`, data);
28
+ return doc;
29
+ }
30
+ }
package/index.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { Application } from "structured-fw/Application";
2
2
  import { config } from './Config.js';
3
+ import { Layout } from "./system/server/Layout.js";
3
4
 
4
5
  const app = new Application(config);
5
6
 
7
+ app.registerPlugin(async (app) => {
8
+
9
+ }, {
10
+ poop: 123
11
+ });
12
+
6
13
  // app.on('afterComponentsLoaded', (components) => {
7
14
  // components.componentNames.forEach((componentName) => {
8
15
  // console.log(componentName)
@@ -18,4 +25,6 @@ app.on('documentCreated', (document) => {
18
25
  document.on('componentCreated', (component) => {
19
26
  document.head.add(`<script>console.log('${component.name}')</script>`);
20
27
  });
21
- })
28
+ });
29
+
30
+ export const layout: Layout = new Layout(app, 'layout');
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.43",
17
+ "version": "0.8.71",
18
18
  "scripts": {
19
19
  "develop": "tsc --watch",
20
20
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
@@ -48,6 +48,8 @@
48
48
  "./Util": "./build/system/Util.js",
49
49
  "./Application": "./build/system/server/Application.js",
50
50
  "./Document": "./build/system/server/Document.js",
51
+ "./Component": "./build/system/server/Component.js",
52
+ "./Layout": "./build/system/server/Layout.js",
51
53
  "./FormValidation": "./build/system/server/FormValidation.js",
52
54
  "./ClientComponent": "./build/system/client/ClientComponent.js"
53
55
  }