5htp-core 0.3.2 → 0.3.3-2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.3.2",
4
+ "version": "0.3.3-2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -61,7 +61,6 @@
61
61
  "mysql2": "^2.3.0",
62
62
  "nodemailer": "^6.6.3",
63
63
  "object-sizeof": "^1.6.3",
64
- "ololog": "^1.1.175",
65
64
  "path-to-regexp": "^6.2.0",
66
65
  "picomatch": "^2.3.1",
67
66
  "preact": "^10.5.15",
@@ -75,6 +74,7 @@
75
74
  "request": "^2.88.2",
76
75
  "sharp": "^0.29.1",
77
76
  "sql-formatter": "^4.0.2",
77
+ "tslog": "^4.9.1",
78
78
  "uuid": "^8.3.2",
79
79
  "uuid-by-string": "^3.0.4",
80
80
  "validator": "^13.7.0",
@@ -65,6 +65,7 @@
65
65
  gap: @spacing / 2;
66
66
  min-width: 4em;
67
67
  font-size: 1rem;
68
+ z-index: 1; // Make the label on top of ::before for example
68
69
 
69
70
  li > & {
70
71
  flex: 1;
@@ -111,7 +112,7 @@
111
112
  }
112
113
 
113
114
  > i {
114
- color: inherit !important;
115
+ color: inherit;
115
116
  }
116
117
 
117
118
  }
@@ -106,23 +106,4 @@ body {
106
106
  -webkit-font-smoothing: antialiased;
107
107
  -moz-osx-font-smoothing: grayscale;
108
108
 
109
- }
110
-
111
- /*#loading {
112
- position: fixed;
113
- top: 0;
114
- left: 0;
115
- width: 100%;
116
- height: 100%;
117
-
118
- display: flex;
119
- flex-direction: column;
120
- align-items: center;
121
- justify-content: center;
122
-
123
- backdrop-filter: blur(10px);
124
- background: fade(#fff, 50%);
125
- color: #000;
126
-
127
- visibility: hidden;
128
- }*/
109
+ }
@@ -185,7 +185,7 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
185
185
  </div>
186
186
  )),
187
187
 
188
- loading: (title: string) => app.loading = show({
188
+ loading: (title: string) => show({
189
189
  title: title,
190
190
  type: 'loading'
191
191
  }),
@@ -180,7 +180,10 @@ export default ({
180
180
  </div>
181
181
 
182
182
  {(enableSearch && choices.length !== 0 && search.keywords.length !== 0) && (
183
- <ul class="row al-left wrap sp-05 pd-1">
183
+ <ul class="row al-left wrap sp-05 pd-1" style={{
184
+ maxHeight: '30vh',
185
+ overflowY: 'auto'
186
+ }}>
184
187
  {choices.map( choice => (
185
188
  <ChoiceElement choice={choice}
186
189
  currentList={currentList}
@@ -14,45 +14,34 @@ import type Page from '../response/page';
14
14
  - PAGE STATE
15
15
  ----------------------------------*/
16
16
 
17
- export default ({ page, isCurrent }: { page: Page, isCurrent?: boolean }) => {
17
+ export default ({ page }: { page: Page }) => {
18
18
 
19
+ /*----------------------------------
20
+ - CONTEXT
21
+ ----------------------------------*/
19
22
  const context = useContext();
20
23
 
21
24
  const [apiData, setApiData] = React.useState<{[k: string]: any} | null>(
22
- page.loading ? null : page.data
25
+ page.data || {}
23
26
  );
24
27
  page.setAllData = setApiData;
25
28
  context.data = apiData;
26
-
27
- React.useEffect(() => {
28
-
29
- // Fetch the data asynchronously for the first time
30
- if (/*apiData === null && */isCurrent)
31
- page.fetchData().then( loadedData => {
32
- page.loading = false;
33
- setApiData(loadedData);
34
- })
35
-
36
- }, [page]);
37
-
38
- /*
39
- <div
40
- class={"page" + (isCurrent ? ' current' : '')}
41
- id={page.chunkId === undefined ? undefined : 'page_' + page.chunkId}
42
- >
43
- */
44
29
 
30
+ /*----------------------------------
31
+ - RENDER
32
+ ----------------------------------*/
45
33
  // Make request parameters and api data accessible from the page component
46
34
  return page.renderer ? (
47
35
 
48
36
  <page.renderer
49
37
  // Services
50
38
  {...context}
51
- // URL params
52
- {...context.request.data}
53
- // API data
54
- data={apiData}
39
+ // API data & URL params
40
+ data={{
41
+ ...apiData,
42
+ ...context.request.data
43
+ }}
55
44
  />
56
45
 
57
- ) : null
46
+ ) : 'Renderer missing'
58
47
  }
@@ -8,7 +8,7 @@ import React from 'react';
8
8
  import useContext from '@/client/context';
9
9
 
10
10
  // Specific
11
- import type Router from '..';
11
+ import type ClientRouter from '..';
12
12
  import PageComponent from './Page';
13
13
  import ClientRequest from '../request';
14
14
  import { history, location, Update } from '../request/history';
@@ -29,29 +29,47 @@ export type PropsPage<TParams extends { [cle: string]: unknown }> = TParams & {
29
29
 
30
30
  const LogPrefix = `[router][component]`
31
31
 
32
+ const PageLoading = ({ clientRouter }: { clientRouter?: ClientRouter }) => {
33
+
34
+ const [isLoading, setLoading] = React.useState(false);
35
+
36
+ if (clientRouter)
37
+ clientRouter.setLoading = setLoading;
38
+
39
+ return isLoading ? (
40
+ <div id="loading">
41
+ <i src="spin" />
42
+ </div>
43
+ ) : null
44
+
45
+ }
46
+
32
47
  /*----------------------------------
33
48
  - COMPONENT
34
49
  ----------------------------------*/
35
- export default ({ service: router }: { service: Router }) => {
50
+ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
36
51
 
37
52
  const context = useContext();
38
53
 
39
54
  const [pages, setPages] = React.useState<{
40
- current: undefined | Page,
41
- //previous: undefined | Page
55
+ current: undefined | Page
42
56
  }>({
43
- current: context.page,
44
- //previous: undefined
57
+ current: context.page
45
58
  });
46
59
 
47
60
  const resolvePage = async (request: ClientRequest, locationUpdate?: Update) => {
48
61
 
62
+ if (!clientRouter) return;
63
+
49
64
  // WARNING: Don"t try to play with pages here, since the object will not be updated
50
65
  // If needed to play with pages, do it in the setPages callback below
66
+
67
+ // Set loading state
68
+ clientRouter.setLoading(true);
51
69
 
52
70
  // Load the route chunks
53
71
  context.request = request;
54
- const newpage = context.page = await router.resolve(request);
72
+ const newpage = context.page = await clientRouter.resolve(request);
55
73
 
56
74
  // Page not found: Directly load with the browser
57
75
  if (newpage === undefined) {
@@ -63,18 +81,18 @@ export default ({ service: router }: { service: Router }) => {
63
81
  return;
64
82
  }
65
83
 
66
- // Set.loading state
67
- newpage.isLoading = true;
68
- newpage.loading = <i src="spin" />
84
+ // Fetch API data to hydrate the page
85
+ const newData = context.data = await newpage.fetchData();
86
+
69
87
  // Add page container
70
88
  setPages( pages => {
71
89
 
72
- const currentRoute = pages.current?.route;
73
-
74
90
  // Check if the page changed
75
- if (currentRoute?.path === request.path) {
91
+ if (pages.current?.chunkId === newpage.chunkId) {
76
92
  console.warn(LogPrefix, "Canceling navigation to the same page:", {...request});
77
- return pages;
93
+ pages.current.setAllData(newData);
94
+ clientRouter.setLoading(false);
95
+ return { ...pages }
78
96
  }
79
97
 
80
98
  // If if the layout changed
@@ -88,24 +106,12 @@ export default ({ service: router }: { service: Router }) => {
88
106
  // Find a way to unload the previous layout / page resources before to load the new one
89
107
  console.log(LogPrefix, `Changing layout. Before:`, curLayout, 'New layout:', newLayout);
90
108
  window.location.replace(request.url);
91
- return pages;
109
+ return { ...pages }
92
110
 
93
111
  context.app.setLayout(newLayout);
94
112
  }
95
113
 
96
- // Remove old page after the aff-page css transition
97
- const oldPage = pages.current;
98
- if (oldPage !== undefined) {
99
- setTimeout(() => setPages({
100
- current: newpage,
101
- //previous: undefined
102
- }), 500);
103
- }
104
-
105
- return {
106
- current: newpage,
107
- //previous: oldPage
108
- }
114
+ return { current: newpage }
109
115
  });
110
116
  }
111
117
 
@@ -116,7 +122,7 @@ export default ({ service: router }: { service: Router }) => {
116
122
  inline: "nearest"
117
123
  })
118
124
 
119
- // First load
125
+ // First render
120
126
  React.useEffect(() => {
121
127
 
122
128
  // Resolve page if it wasn't done via SSR
@@ -131,13 +137,18 @@ export default ({ service: router }: { service: Router }) => {
131
137
  await resolvePage(request);
132
138
 
133
139
  // Scroll to the selected content via url hash
134
- restoreScroll(pages.current);
140
+ //restoreScroll(pages.current);
135
141
  })
136
142
  }, []);
137
143
 
138
144
  // On every page change
139
145
  React.useEffect(() => {
140
146
 
147
+ if (!clientRouter) return;
148
+
149
+ // Page loaded
150
+ clientRouter.setLoading(false);
151
+
141
152
  // Reset scroll
142
153
  window.scrollTo(0, 0);
143
154
  // Should be called AFTER rendering the page (so after the state change)
@@ -146,10 +157,10 @@ export default ({ service: router }: { service: Router }) => {
146
157
  restoreScroll(pages.current);
147
158
 
148
159
  // Hooks
149
- router.runHook('page.changed', pages.current)
160
+ clientRouter.runHook('page.changed', pages.current)
150
161
 
151
162
  }, [pages.current]);
152
-
163
+
153
164
  // Render the page component
154
165
  return <>
155
166
  {/*pages.previous && (
@@ -158,9 +169,12 @@ export default ({ service: router }: { service: Router }) => {
158
169
 
159
170
  {pages.current && (
160
171
  <PageComponent page={pages.current}
161
- isCurrent
162
- key={pages.current.id === undefined ? undefined : 'page_' + pages.current.id}
172
+ /* Create a new instance of the Page component every time the page change
173
+ Otherwise the page will memorise the data of the previous page */
174
+ key={pages.current.chunkId === undefined ? undefined : 'page_' + pages.current.chunkId}
163
175
  />
164
176
  )}
177
+
178
+ <PageLoading clientRouter={clientRouter} />
165
179
  </>
166
180
  }
@@ -48,9 +48,9 @@ export default class ApiClient implements ApiClientService {
48
48
  - HIGH LEVEL
49
49
  ----------------------------------*/
50
50
 
51
- public fetch<TProvidedData extends TFetcherList = TFetcherList>(
52
- fetchers: TFetcherList
53
- ): TDataReturnedByFetchers<TProvidedData> {
51
+ public fetch<FetchersList extends TFetcherList = TFetcherList>(
52
+ fetchers: FetchersList
53
+ ): TDataReturnedByFetchers<FetchersList> {
54
54
  throw new Error("api.fetch shouldn't be called here.");
55
55
  }
56
56
 
@@ -24,8 +24,6 @@ import type ClientRouter from '..';
24
24
 
25
25
  export default class ClientPage<TRouter = ClientRouter> extends PageResponse<TRouter> {
26
26
 
27
- public isLoading: boolean = false;
28
- public loading: false | ComponentChild;
29
27
  public scrollToId: string;
30
28
 
31
29
  public constructor(
@@ -45,11 +43,9 @@ export default class ClientPage<TRouter = ClientRouter> extends PageResponse<TRo
45
43
 
46
44
  // Add the page to the context
47
45
  this.context.page = this;
48
- this.isLoading = true;
49
46
 
50
47
  // Data succesfully loaded
51
48
  this.data = data || await this.fetchData();
52
- this.isLoading = false;
53
49
 
54
50
  return this;
55
51
  }
@@ -91,15 +91,15 @@ export type TRouteOptions = {
91
91
  // Resolving
92
92
  domain?: string,
93
93
  accept?: string,
94
-
95
- // Access Restriction
96
94
  auth?: TUserRole | boolean,
97
- //form?: TSchema,
95
+
96
+ // Rendering
97
+ static?: boolean,
98
98
  layout?: false | string, // The nale of the layout
99
99
 
100
+ // To cleanup
100
101
  TESTING?: boolean,
101
102
  logging?: boolean,
102
-
103
103
  }
104
104
 
105
105
  export type TRouteModule<TRegisteredRoute = any> = {
@@ -14,11 +14,7 @@ import type { FileToUpload } from '@client/components/inputv3/file';
14
14
  // By example if we want to fetch an api endpoint only if the url contains a certain url parameter
15
15
  export type TFetcherList = { [id: string]: TFetcher | undefined }
16
16
 
17
- export type TPostData = {[key: string]: PrimitiveValue}
18
-
19
- export type TPostDataWithFile = {[key: string]: PrimitiveValue | FileToUpload}
20
-
21
- export type TFetcher<TData extends unknown = unknown> = {
17
+ export type TFetcher<TData extends any = unknown> = {
22
18
 
23
19
  // For async calls: api.post(...).then((data) => ...)
24
20
  then: (callback: (data: TData) => void) => Promise<TData>,
@@ -44,12 +40,16 @@ export type TApiFetchOptions = {
44
40
  encoding?: 'json' | 'multipart'
45
41
  }
46
42
 
43
+ export type TPostData = {[key: string]: PrimitiveValue}
44
+
45
+ export type TPostDataWithFile = {[key: string]: PrimitiveValue | FileToUpload}
46
+
47
47
  // https://stackoverflow.com/questions/44851268/typescript-how-to-extract-the-generic-parameter-from-a-type
48
48
  type TypeWithGeneric<T> = TFetcher<T>
49
49
  type extractGeneric<Type> = Type extends TypeWithGeneric<infer X> ? X : never
50
50
 
51
51
  export type TDataReturnedByFetchers<TProvidedData extends TFetcherList = {}> = {
52
- [Property in keyof TProvidedData]: undefined | (extractGeneric<TProvidedData[Property]> extends ((...args: any[]) => any)
52
+ [Property in keyof TProvidedData]: (extractGeneric<TProvidedData[Property]> extends ((...args: any[]) => any)
53
53
  ? ThenArg<ReturnType< extractGeneric<TProvidedData[Property]> >>
54
54
  : extractGeneric<TProvidedData[Property]>
55
55
  )
@@ -35,7 +35,7 @@ export type TFrontRenderer<
35
35
  TAdditionnalData
36
36
  &
37
37
  {
38
- data: TDataReturnedByFetchers<TProvidedData>
38
+ data: {[key: string]: PrimitiveValue}
39
39
  }
40
40
  )
41
41
  ) => VNode<any> | null
@@ -125,18 +125,20 @@ export class Application<
125
125
  protected async start() {
126
126
 
127
127
  console.log(`5HTP Core`, process.env.npm_package_version);
128
+ const startTime = Date.now();
128
129
 
129
130
  // Handle errors & crashs
130
131
  this.on('error', e => this.Console.createBugReport(e))
131
132
 
132
133
  console.info(`[boot] Start services`);
133
134
  await this.startServices();
135
+ this.debug && console.info(`[boot] Services are ready`);
134
136
 
135
- this.debug && console.info(`[boot] App ready`);
136
137
  await this.ready();
137
138
  await this.runHook('ready');
138
139
 
139
- this.debug && console.info(`[boot] Application is ready.`);
140
+ const startedTime = (Date.now() - startTime) / 1000;
141
+ console.info(`[boot] Application launched in ${startedTime}s`);
140
142
  this.launched = true;
141
143
  }
142
144
 
@@ -106,7 +106,8 @@ export class ServicesContainer<
106
106
  ...Object.getOwnPropertyNames( Object.getPrototypeOf( instance )),
107
107
  ...Object.getOwnPropertyNames( instance ),
108
108
  // service.launch() isn't included, maybe because parent abstract class
109
- 'launch'
109
+ 'launch',
110
+ 'bindServices'
110
111
  ];
111
112
 
112
113
  for (const method of methods)
@@ -83,8 +83,7 @@ export default abstract class Service<
83
83
  : app
84
84
 
85
85
  // Instanciate subservices
86
- for (const localName in services)
87
- this.bindService( localName, services[localName] as unknown as TRegisteredService );
86
+ this.bindServices( services );
88
87
 
89
88
  }
90
89
 
@@ -155,7 +154,7 @@ export default abstract class Service<
155
154
  ReturnType< TServiceClass["getServiceInstance"] >
156
155
  &
157
156
  {
158
- new (...args: any[]): TServiceClass & { services: TSubServices },
157
+ new (...args: any[]): TServiceClass["getServiceInstance"] & { services: TSubServices },
159
158
  services: TSubServices
160
159
  }
161
160
  /*Omit<TServiceClass, 'services'> & {
@@ -169,12 +168,17 @@ export default abstract class Service<
169
168
  throw new Error(`Unable to use service "${serviceId}": This one hasn't been setup.`);
170
169
 
171
170
  // Bind subservices
172
- registered.subServices = subServices || {};
171
+ if (subServices !== undefined)
172
+ registered.subServices = {
173
+ ...registered.subServices,
174
+ ...subServices
175
+ };
173
176
 
174
177
  // Check if not already instanciated
175
178
  const existing = ServicesContainer.allServices[ serviceId ];
176
179
  if (existing !== undefined) {
177
180
  console.info("Service", serviceId, "already instanciated through another service.");
181
+ existing.bindServices( registered.subServices );
178
182
  return existing;
179
183
  }
180
184
 
@@ -183,14 +187,25 @@ export default abstract class Service<
183
187
  const ServiceClass = registered.metas.class().default;
184
188
 
185
189
  // Create class instance
186
- const service = new ServiceClass(this, registered.config, registered.subServices, this.app || this)
187
- .getServiceInstance()
190
+ const service = new ServiceClass(
191
+ this,
192
+ registered.config,
193
+ registered.subServices,
194
+ this.app || this
195
+ )
196
+ const serviceInstance = service.getServiceInstance();
188
197
 
189
198
  // Bind his own metas
190
199
  service.metas = registered.metas;
191
- ServicesContainer.allServices[ registered.metas.id ] = service;
200
+ ServicesContainer.allServices[ registered.metas.id ] = serviceInstance;
201
+
202
+ return serviceInstance;
203
+ }
192
204
 
193
- return service;
205
+ public bindServices( services: TServicesIndex ) {
206
+
207
+ for (const localName in services)
208
+ this.bindService( localName, services[localName] as unknown as TRegisteredService );
194
209
  }
195
210
 
196
211
  public bindService( localName: string, service: AnyService ) {
@@ -5,7 +5,7 @@
5
5
  "baseUrl": "..",
6
6
  "paths": {
7
7
 
8
- "@/server/models": ["./server/.generated/models"],
8
+ "@/server/models": ["./server/.generated/models.d.ts"],
9
9
 
10
10
  "@client/*": ["../node_modules/5htp-core/src/client/*"],
11
11
  "@common/*": ["../node_modules/5htp-core/src/common/*"],
@@ -3,8 +3,8 @@
3
3
  ----------------------------------*/
4
4
  // Npm
5
5
  import Ansi2Html from 'ansi-to-html';
6
- import { Logger, ILogObject } from "tslog"
7
- import type { Console } from '.';;
6
+ import { formatWithOptions } from 'util';
7
+ import type { default as Console, TJsonLog } from '.';;
8
8
 
9
9
  var ansi2Html = new Ansi2Html({
10
10
  newline: true,
@@ -35,30 +35,31 @@ var ansi2Html = new Ansi2Html({
35
35
  /*----------------------------------
36
36
  - METHOD
37
37
  ----------------------------------*/
38
- export default (log: ILogObject, c: Console) => {
38
+ export default (log: TJsonLog, c: Console) => {
39
39
 
40
- if (log.logLevel === 'error') {
40
+ // Print metas as ANSI
41
+ const logMetaMarkup = c.logger._prettyFormatLogObjMeta({
42
+ date: log.time,
43
+ logLevelId: c.getLogLevelId( log.level ),
44
+ logLevelName: log.level,
45
+ // We consider that having the path is useless in this case
46
+ path: undefined,
47
+ });
41
48
 
42
- // Enrichissement erreurs
43
- for (const arg of log.argumentsArray) {
44
-
45
- // Chemin complet pour pouvoir l'ouvrir dans un éditeyr via un clic
46
- if (typeof arg === 'object' && arg.stack !== undefined) for (const stack of arg.stack)
47
- stack.filePath = stack.fullFilePath;
48
-
49
- }
50
- }
51
-
52
- // BUG: log.date pas pris encompte, affiche la date actuelle
53
- // https://github.com/fullstack-build/tslog/blob/master/src/LoggerWithoutCallSite.ts#L509
54
-
55
- let ansi: string = '';
56
- const myStd = { write: (message: string) => ansi += message }
57
- c.logger.printPrettyLog(myStd, log);
49
+ // Print args as ANSI
50
+ const logArgsAndErrorsMarkup = c.logger.runtime.prettyFormatLogObj( log.args, c.logger.settings);
51
+ const logErrors = logArgsAndErrorsMarkup.errors;
52
+ const logArgs = logArgsAndErrorsMarkup.args;
53
+ const logErrorsStr = (logErrors.length > 0 && logArgs.length > 0 ? "\n" : "") + logErrors.join("\n");
54
+ c.logger.settings.prettyInspectOptions.colors = c.logger.settings.stylePrettyLogs;
55
+ let ansi = logMetaMarkup + formatWithOptions(c.logger.settings.prettyInspectOptions, ...logArgs) + logErrorsStr;
58
56
 
57
+ // Use HTML spaces
59
58
  ansi = ansi.replace(/ {2}/g, '&nbsp;');
60
59
  ansi = ansi.replace(/\t/g, '&nbsp;'.repeat(8));
60
+ ansi = ansi.replace(/\n/g, '<br/>');
61
61
 
62
+ // Convert ANSI to HTML
62
63
  const html = ansi2Html.toHtml(ansi)
63
64
 
64
65
  return html;
@@ -1,9 +1,14 @@
1
1
  /*----------------------------------
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
+
5
+ // Node
6
+ import { serialize } from 'v8';
7
+ import { formatWithOptions } from 'util';
8
+
4
9
  // Npm
5
10
  import { v4 as uuid } from 'uuid';
6
- import log from 'ololog';
11
+ import { Logger, IMeta, ILogObj, ISettings } from 'tslog';
7
12
  import { format as formatSql } from 'sql-formatter';
8
13
  import highlight from 'cli-highlight';
9
14
 
@@ -85,8 +90,14 @@ export type TDbQueryLog = ChannelInfos & {
85
90
  time: number,
86
91
  }
87
92
 
88
- export type TLog = ILogObject & ChannelInfos
93
+ export type TLogLevel = keyof typeof logLevels
89
94
 
95
+ export type TJsonLog = {
96
+ time: Date,
97
+ level: TLogLevel,
98
+ args: unknown[],
99
+ channel: ChannelInfos
100
+ }
90
101
 
91
102
  /*----------------------------------
92
103
  - CONST
@@ -96,21 +107,12 @@ const LogPrefix = '[console]'
96
107
 
97
108
  const errorMailInterval = (1 * 60 * 60 * 1000); // 1 hour
98
109
 
99
- const logFields = [
100
- 'date',
101
- 'logLevelId',
102
- 'logLevel',
103
-
104
- 'isConstructor',
105
- 'methodName',
106
- 'functionName',
107
- 'typeName',
108
-
109
- 'filePath',
110
- 'lineNumber',
111
- 'argumentsArray',
112
- 'stack',
113
- ] as const
110
+ const logLevels = {
111
+ 'log': 0,
112
+ 'info': 3,
113
+ 'warn': 4,
114
+ 'error': 5
115
+ } as const
114
116
 
115
117
  /*----------------------------------
116
118
  - LOGGER
@@ -118,21 +120,16 @@ const logFields = [
118
120
  export default class Console extends Service<Config, Hooks, Application, Services> {
119
121
 
120
122
  // Services
121
- public logger!: Logger;
122
-
123
+ public logger!: Logger<ILogObj>;
123
124
  // Buffers
124
- public logs: TLog[] = [];
125
- public clients: TGuestLogs[] = [];
126
- public requests: TRequestLogs[] = [];
127
- public sqlQueries: TDbQueryLog[] = [];
125
+ public logs: TJsonLog[] = [];
128
126
  // Bug ID => Timestamp latest send
129
127
  private sentBugs: {[bugId: string]: number} = {};
130
128
 
131
- // Adapters
132
- public log = console.log;
133
- public warn = console.warn;
134
- public info = console.info;
135
- public error = console.error;
129
+ // Old (still useful???)
130
+ /*public clients: TGuestLogs[] = [];
131
+ public requests: TRequestLogs[] = [];
132
+ public sqlQueries: TDbQueryLog[] = [];*/
136
133
 
137
134
  /*----------------------------------
138
135
  - LIFECYCLE
@@ -140,34 +137,62 @@ export default class Console extends Service<Config, Hooks, Application, Service
140
137
 
141
138
  protected async start() {
142
139
 
143
- const envConfig = this.config[ this.app.env.profile ];
144
-
145
- /*const origConsole = console;
146
- console.log = (...args: unknown[]) => log(...args)*/
140
+ const origLog = console.log
147
141
 
148
- /*this.logger = new Logger({
149
- overwriteConsole: true,
150
- //type: this.app.env.profile === 'dev' ? 'pretty' : 'hidden',
151
- requestId: (): string => {
152
- const { channelType, channelId } = this.getChannel();
153
- return channelId === undefined ? channelType : channelType + ':' + channelId;
154
- },
155
- displayRequestId: false,
142
+ this.logger = new Logger({
143
+ // Use to improve performance in production
156
144
  hideLogPositionForProduction: this.app.env.profile === 'prod',
145
+ type: 'pretty',
157
146
  prettyInspectOptions: {
158
147
  depth: 2
148
+ },
149
+ overwrite: {
150
+ formatMeta: (meta?: IMeta) => {
151
+
152
+ // Shorten file paths
153
+ if (meta?.path !== undefined) {
154
+ meta.path.filePathWithLine = this.shortenFilePath( meta.path.filePathWithLine );
155
+ }
156
+
157
+ return this.logger._prettyFormatLogObjMeta( meta );
158
+ },
159
+ transportFormatted: (
160
+ logMetaMarkup: string,
161
+ logArgs: unknown[],
162
+ logErrors: string[],
163
+ settings: ISettings<ILogObj>
164
+ ) => {
165
+ const logErrorsStr = (logErrors.length > 0 && logArgs.length > 0 ? "\n" : "") + logErrors.join("\n");
166
+ settings.prettyInspectOptions.colors = settings.stylePrettyLogs;
167
+ origLog(logMetaMarkup + formatWithOptions(settings.prettyInspectOptions, ...logArgs) + logErrorsStr);
168
+ },
159
169
  }
160
- });
161
-
162
- this.logger.attachTransport({
163
- silly: this.logEntry.bind(this),
164
- debug: this.logEntry.bind(this),
165
- trace: this.logEntry.bind(this),
166
- info: this.logEntry.bind(this),
167
- warn: this.logEntry.bind(this),
168
- error: this.logEntry.bind(this),
169
- fatal: this.logEntry.bind(this),
170
- }, envConfig.level);*/
170
+ });
171
+
172
+ if (console["_wrapped"] !== undefined)
173
+ return;
174
+
175
+ for (const logLevel in logLevels) {
176
+ console[ logLevel ] = (...args: any[]) => {
177
+
178
+ // Dev mode = no care about performance = rich logging
179
+ if (this.app.env.profile === 'dev' || ['warn', 'error'].includes( logLevel ))
180
+ //this.logger[ logLevel ](...args);
181
+ origLog(...args);
182
+ // Prod mode = minimal logging
183
+
184
+ const channel = this.getChannel();
185
+
186
+ this.logs.push({
187
+ time: new Date,
188
+ level: logLevel,
189
+ args,
190
+ channel
191
+ });
192
+ }
193
+ }
194
+
195
+ console["_wrapped"] = true;
171
196
 
172
197
  setInterval(() => this.clean(), 10000);
173
198
  }
@@ -180,15 +205,50 @@ export default class Console extends Service<Config, Hooks, Application, Service
180
205
 
181
206
  }
182
207
 
208
+ /*----------------------------------
209
+ - LOGS FORMATTING
210
+ ----------------------------------*/
211
+
212
+ public shortenFilePath( filepath?: string ) {
213
+
214
+ if (filepath === undefined)
215
+ return undefined;
216
+
217
+ const projectRoot = this.app.container.path.root;
218
+ if (filepath.startsWith( projectRoot ))
219
+ filepath = filepath.substring( projectRoot.length )
220
+
221
+ const frameworkRoot = '/node_modules/5htp-core/src/';
222
+ if (filepath.startsWith( frameworkRoot ))
223
+ filepath = '@' + filepath.substring( frameworkRoot.length )
224
+
225
+ return filepath;
226
+
227
+ }
228
+
229
+
183
230
  /*----------------------------------
184
231
  - ACTIONS
185
232
  ----------------------------------*/
186
233
 
234
+ public getLogLevelId( logLevelName: TLogLevel ) {
235
+ return logLevels[ logLevelName ]
236
+ }
237
+
187
238
  private clean() {
188
- /*this.config.debug && console.log(LogPrefix, `Clean logs buffer. Current size:`, this.logs.length, '/', this.config.bufferLimit);
239
+
240
+ if (this.config.debug) {
241
+ console.log(
242
+ LogPrefix,
243
+ `Clean logs buffer. Current size:`,
244
+ this.logs.length, '/', this.config.bufferLimit,
245
+ 'Memory Size:', serialize(this.logs).byteLength
246
+ );
247
+ }
248
+
189
249
  const bufferOverflow = this.logs.length - this.config.bufferLimit;
190
250
  if (bufferOverflow > 0)
191
- this.logs = this.logs.slice(bufferOverflow);*/
251
+ this.logs = this.logs.slice(bufferOverflow);
192
252
  }
193
253
 
194
254
  public async createBugReport( error: Error, request?: ServerRequest ) {
@@ -223,7 +283,7 @@ export default class Console extends Service<Config, Hooks, Application, Service
223
283
  // On envoi l'email avant l'insertion dans bla bdd
224
284
  // Car cette denrière a plus de chances de provoquer une erreur
225
285
  const logsHtml = this.printHtml(
226
- this.logs/*.filter(e => e.channelId === channelId)*/.slice(-100),
286
+ this.logs.filter( e => e.channel.channelId === channelId).slice(-100),
227
287
  true
228
288
  );
229
289
 
@@ -252,95 +312,10 @@ export default class Console extends Service<Config, Hooks, Application, Service
252
312
  }
253
313
  }
254
314
 
255
- private logEntry(entry: ILogObject) {
256
-
257
- // Don't keep logs from the admin sashboard
258
- const [channelType, channelId] = entry.requestId?.split(':') || ['master'];
259
- if (entry.requestId === 'admin')
260
- return;
261
-
262
- // Only keep data required by printPrettyLog
263
- // https://github.com/fullstack-build/tslog/blob/4f045d61333230bd0f9db0e0d59cb1e81fc03aa6/src/LoggerWithoutCallSite.ts#L509
264
- const miniLog: TObjetDonnees = { channelType, channelId };
265
- for (const k of logFields)
266
- miniLog[k] = entry[k];
267
-
268
- this.logs.push(miniLog as TLog);
269
- }
270
-
271
- public client( client: TGuestLogs ) {
272
- this.clients.push(client);
273
- }
274
-
275
- public request( request: TRequestLogs ) {
276
-
277
- if (request.id === 'admin')
278
- return;
279
-
280
- this.requests.push( request );
281
- }
282
-
283
- public database( dbQuery: TDbQueryLog ) {
284
-
285
- this.requests.push( dbQuery );
286
- }
287
-
288
315
  /*----------------------------------
289
316
  - READ
290
317
  ----------------------------------*/
291
318
 
292
- /*public getClients() {
293
- return sql`
294
- SELECT * FROM logs.Clients
295
- ORDER BY activity DESC
296
- LIMIT 100
297
- `.all();
298
- }
299
-
300
- public async getClient(clientId: string) {
301
- return (
302
- this.clients.find(c => c.id === clientId)
303
- ||
304
- await sql`
305
- SELECT * FROM logs.Clients
306
- WHERE id = ${clientId}
307
- `.first()
308
- )
309
- }
310
-
311
- public getRequests(clientId?: string) {
312
- return sql`
313
- SELECT * FROM logs.Requests
314
- ORDER BY date DESC
315
- LIMIT 100
316
- `.all();
317
- }
318
-
319
- public async getRequest(requestId: string) {
320
- return (
321
- this.requests.find(r => r.id === requestId)
322
- ||
323
- await sql`
324
- SELECT * FROM logs.Requests
325
- WHERE id = ${requestId}
326
- `.first()
327
- )
328
- }
329
-
330
- public getQueries( channelType: ChannelInfos["channelType"], channelId?: string ) {
331
-
332
- const filters: Partial<TDbQueryLog> = { channelType };
333
- if (channelId !== undefined)
334
- filters.channelId = channelId;
335
-
336
- return sql`
337
- SELECT * FROM logs.Queries
338
- WHERE :${filters}
339
- ORDER BY date DESC
340
- LIMIT 100
341
- `.all();
342
- }*/
343
-
344
319
  public async getLogs( channelType: ChannelInfos["channelType"], channelId?: string ) {
345
320
 
346
321
  const filters: Partial<TDbQueryLog> = { channelType };
@@ -373,7 +348,7 @@ export default class Console extends Service<Config, Hooks, Application, Service
373
348
  return this.printHtml( entries );
374
349
  }
375
350
 
376
- public printHtml(logs: TLog[], full: boolean = false): string {
351
+ public printHtml( logs: TJsonLog[], full: boolean = false ): string {
377
352
 
378
353
  let html = logs.map( logEntry => logToHTML( logEntry, this )).join('\n');
379
354
 
@@ -9,7 +9,7 @@ import cronParser, { CronExpression } from 'cron-parser';
9
9
  - TYPES
10
10
  ----------------------------------*/
11
11
 
12
- import type { CronManager } from '.';
12
+ import type CronManager from '.';
13
13
 
14
14
  export type TFrequence = string | Date;
15
15
  export type TRunner = () => Promise<any>
@@ -22,17 +22,15 @@ export default class CronTask {
22
22
  public cron?: CronExpression
23
23
  public nextInvocation?: Date;
24
24
 
25
- private debug?: boolean;
26
-
27
25
  public constructor(
28
- manager: CronManager,
26
+ private manager: CronManager,
29
27
  public nom: string,
30
28
  next: TFrequence,
31
29
  public runner: TRunner,
32
30
  public autoexec?: boolean
33
31
  ) {
34
32
 
35
- console.info(`[cron][${this.nom}] Enregistrement de la tâche`);
33
+ this.manager.config.debug && console.info(`[cron][${this.nom}] Enregistrement de la tâche`);
36
34
 
37
35
  this.schedule(next);
38
36
 
@@ -48,13 +46,15 @@ export default class CronTask {
48
46
  this.cron = cronParser.parseExpression(next);
49
47
  this.nextInvocation = this.cron.next().toDate();
50
48
 
51
- console.info(`[cron][${this.nom}] Planifié pour ${this.nextInvocation.toISOString()} via cron ${next}`);
49
+ this.manager.config.debug &&
50
+ console.info(`[cron][${this.nom}] Planifié pour ${this.nextInvocation.toISOString()} via cron ${next}`);
52
51
 
53
52
  // Date
54
53
  } else {
55
54
 
56
55
  this.nextInvocation = next;
57
- console.info(`[cron][${this.nom}] Planifié pour ${this.nextInvocation.toISOString()} via date`);
56
+ this.manager.config.debug &&
57
+ console.info(`[cron][${this.nom}] Planifié pour ${this.nextInvocation.toISOString()} via date`);
58
58
 
59
59
  }
60
60
  }
@@ -80,7 +80,7 @@ export default class CronTask {
80
80
 
81
81
  // Execution
82
82
  this.runner().then(() => {
83
- console.info(`Tâche executée.`);
83
+ this.manager.config.debug && console.info(`Task runned.`);
84
84
  })
85
85
  }
86
86
  }
@@ -21,7 +21,7 @@ export { default as CronTask } from './CronTask';
21
21
  ----------------------------------*/
22
22
 
23
23
  export type Config = {
24
-
24
+ debug?: boolean
25
25
  }
26
26
 
27
27
  export type Hooks = {
@@ -46,8 +46,6 @@ export default class CronManager extends Service<Config, Hooks, Application, Ser
46
46
  ----------------------------------*/
47
47
 
48
48
  protected async start() {
49
-
50
- this.app.on('cleanup', () => this.cleanup());
51
49
 
52
50
  clearInterval(CronManager.timer);
53
51
  CronManager.timer = setInterval(() => {
@@ -111,8 +111,6 @@ const LogPrefix = '[database][meta]';
111
111
  const sqlTypeParamsReg = /\'([^\']+)\'\,?/gi;
112
112
  const typeViaCommentReg = /\[type=([a-z]+)\]/g;
113
113
 
114
- const modelsTypesPath = process.cwd() + '/src/server/models.ts';
115
-
116
114
  /*----------------------------------
117
115
  - FUNCTIONS
118
116
  ----------------------------------*/
@@ -368,11 +366,11 @@ export default class MySQLMetasParser {
368
366
  }
369
367
  }
370
368
 
369
+ // Given that this file is updated during run time,
370
+ // We output a typescript ambient file, so the file change doest trigger infinite app reload
371
371
  fs.outputFileSync(
372
- path.join( Container.path.server.generated, 'models.ts'),
372
+ path.join( Container.path.server.generated, 'models.d.ts'),
373
373
  types.join('\n')
374
374
  );
375
- this.debug && console.log(LogPrefix, `Wrote database types to ${modelsTypesPath}`);
376
-
377
375
  }
378
376
  }
@@ -121,8 +121,15 @@ export default class HttpServer {
121
121
  serveStatic: {
122
122
  setHeaders: function setCustomCacheControl(res, path) {
123
123
 
124
+ const dontCache = [
125
+ '/public/icons',
126
+ '/public/client'
127
+ ]
128
+
129
+ res.setHeader('Cache-Control', 'public, max-age=0');
130
+
124
131
  // Set long term cache, except for non-hashed filenames
125
- /*if (__DEV__ || path.includes('/icons.')) {
132
+ /*if (dontCache.some( p => path.startsWith( p ))) {
126
133
  res.setHeader('Cache-Control', 'public, max-age=0');
127
134
  } else {
128
135
  res.setHeader('Cache-Control', 'public, max-age=604800000'); // 7 Days
@@ -126,6 +126,9 @@ export default class ServerRouter<
126
126
  public errors: { [code: number]: TErrorRoute } = {};
127
127
  public ssrRoutes: TSsrUnresolvedRoute[] = [];
128
128
 
129
+ // Cache (ex: for static pages)
130
+ public cache: {[pageId: string]: string} = {}
131
+
129
132
  /*----------------------------------
130
133
  - SERVICE
131
134
  ----------------------------------*/
@@ -97,25 +97,40 @@ export default class ServerResponse<
97
97
  // Create response context for controllers
98
98
  const context = await this.createContext(route);
99
99
 
100
+ // Static rendering
101
+ const chunkId = route.options["id"];
102
+ if (route.options.static &&
103
+ chunkId !== undefined
104
+ &&
105
+ this.router.cache[ chunkId ] !== undefined
106
+ ) {
107
+ await this.html( this.router.cache[ chunkId ] );
108
+ return;
109
+ }
110
+
100
111
  // Run controller
101
- const response = await this.route.controller( context );
112
+ const content = await this.route.controller( context );
102
113
 
103
- // Handle response type
104
- if (response === undefined)
114
+ // Handle content type
115
+ if (content === undefined)
105
116
  return;
106
117
 
107
- // No need to process the response
108
- if (response instanceof ServerResponse)
118
+ // No need to process the content
119
+ if (content instanceof ServerResponse)
109
120
  return;
110
121
  // Render react page to html
111
- else if (response instanceof Page)
112
- await this.render(response, context, additionnalData);
122
+ else if (content instanceof Page)
123
+ await this.render(content, context, additionnalData);
113
124
  // Return HTML
114
- else if (typeof response === 'string' && this.route.options.accept === 'html')
115
- await this.html(response);
125
+ else if (typeof content === 'string' && this.route.options.accept === 'html')
126
+ await this.html(content);
116
127
  // Return JSON
117
128
  else
118
- await this.json(response);
129
+ await this.json(content);
130
+
131
+ // Cache
132
+ if (route.options.static)
133
+ this.router.cache[ chunkId ] = this.data;
119
134
  }
120
135
 
121
136
  /*----------------------------------
@@ -36,10 +36,12 @@ declare module "*.svg" {
36
36
  export = value;
37
37
  }
38
38
 
39
- declare module "*.(png|webp)" {
40
- const value: string;
41
- export = value;
42
- }
39
+ declare module "*.png";
40
+ declare module "*.jpg";
41
+ declare module "*.jpeg";
42
+ declare module "*.webp";
43
+ declare module "*.gif";
44
+ declare module "*.bmp";
43
45
 
44
46
  declare module "*.mp3" {
45
47
  const value: string;
@@ -40,5 +40,5 @@
40
40
  "react/jsx-runtime": ["preact/jsx-runtime"]
41
41
  },
42
42
  },
43
- "include": ["src"]
43
+ "include": ["src", "types/global"]
44
44
  }
File without changes