structured-fw 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Config.ts CHANGED
@@ -42,5 +42,19 @@ export const config: StructuredConfig = {
42
42
  // used by Document.push, can be preload or preconnect
43
43
  linkHeaderRel : 'preload'
44
44
  },
45
+ gzip: {
46
+ enabled: true, // whether to enable response gzip compression
47
+ // compress only listed types
48
+ types: [
49
+ 'text/html',
50
+ 'text/xml',
51
+ 'text/plain',
52
+ 'text/css',
53
+ 'application/javascript',
54
+ 'application/json'
55
+ ],
56
+ minSize: 10240, // compress only if response is at least minSize bytes
57
+ compressionLevel: 4, // higher value = greater compression, slower
58
+ },
45
59
  runtime: 'Node.js'
46
60
  }
package/README.md CHANGED
@@ -290,7 +290,7 @@ type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
290
290
  getArgs: PostedDataDecoded,
291
291
 
292
292
  // send given data as a response
293
- respondWith: (data: any) => void,
293
+ respondWith: (data: any) => Promise<void>,
294
294
 
295
295
  // redirect to given url, with given statusCode
296
296
  redirect: (to: string, statusCode?: number) => void,
@@ -158,7 +158,7 @@ export class Application {
158
158
  prev[curr] = document.children[0].data[curr];
159
159
  return prev;
160
160
  }, {}) : {});
161
- ctx.respondWith({
161
+ await ctx.respondWith({
162
162
  html: document.children[0].dom[unwrap ? 'innerHTML' : 'outerHTML'],
163
163
  initializers: document.initInitializers(),
164
164
  data: exportedData
@@ -24,7 +24,7 @@ export declare class Document extends Component<{
24
24
  body(): string;
25
25
  initInitializers(): Record<string, string>;
26
26
  private initClientConfig;
27
- toString(): string;
27
+ toString(): Promise<string>;
28
28
  allocateId(): string;
29
29
  loadView(pathRelative: string, data?: LooseObject): Promise<Document>;
30
30
  loadComponent(componentName: string, data?: LooseObject): Promise<Document>;
@@ -59,8 +59,8 @@ export class Document extends Component {
59
59
  const clientConfString = `<script type="application/javascript">window.structuredClientConfig = ${JSON.stringify(clientConf)}</script>`;
60
60
  this.head.add(clientConfString);
61
61
  }
62
- toString() {
63
- this.emit('beforeRender');
62
+ async toString() {
63
+ await this.emit('beforeRender');
64
64
  if (!this.initializersInitialized) {
65
65
  this.initInitializers();
66
66
  this.initClientConfig();
@@ -20,4 +20,5 @@ export declare class Request {
20
20
  static queryStringDecode(queryString: string, initialValue?: PostedDataDecoded, trimValues?: boolean): PostedDataDecoded;
21
21
  static parseBodyMultipart(bodyRaw: string, boundary: string): PostedDataDecoded;
22
22
  static multipartBodyFiles(bodyRaw: string, boundary: string): Record<string, RequestBodyFile>;
23
+ private sendResponse;
23
24
  }
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Buffer } from "node:buffer";
5
5
  import { Document } from "./Document.js";
6
+ import zlib from "node:zlib";
6
7
  export class Request {
7
8
  constructor(app) {
8
9
  this.pageNotFoundCallback = async ({ response }) => {
@@ -107,23 +108,21 @@ export class Request {
107
108
  cookies: this.app.cookies.parse(request),
108
109
  timeStart: new Date().getTime(),
109
110
  isAjax: request.headers['x-requested-with'] == 'xmlhttprequest',
110
- respondWith: function (data) {
111
+ respondWith: async (data) => {
111
112
  if (typeof data === 'string' || Buffer.isBuffer(data)) {
112
- response.write(data);
113
+ this.sendResponse(request, response, data, 'text/plain; charset=utf-8');
113
114
  }
114
115
  else if (typeof data === 'number') {
115
- response.write(data.toString());
116
+ this.sendResponse(request, response, data.toString(), 'text/plain; charset=utf-8');
116
117
  }
117
118
  else if (data instanceof Document) {
118
- response.setHeader('Content-Type', 'text/html');
119
- response.write(data.toString());
119
+ this.sendResponse(request, response, await data.toString(), 'text/html; charset=utf-8');
120
120
  }
121
121
  else if (data === undefined || data === null) {
122
- response.write('');
122
+ this.sendResponse(request, response, '', 'text/plain; charset=utf-8');
123
123
  }
124
124
  else {
125
- response.setHeader('Content-Type', 'application/json');
126
- response.write(JSON.stringify(data, null, 4));
125
+ this.sendResponse(request, response, JSON.stringify(data, null, 4), 'application/json; charset=utf-8');
127
126
  }
128
127
  },
129
128
  redirect: (to, statusCode = 302) => {
@@ -134,7 +133,7 @@ export class Request {
134
133
  response.statusCode = 404;
135
134
  const res = await this.pageNotFoundCallback.apply(this.app, [context]);
136
135
  if (res instanceof Document) {
137
- context.respondWith(res);
136
+ await context.respondWith(res);
138
137
  }
139
138
  }
140
139
  };
@@ -157,7 +156,7 @@ export class Request {
157
156
  try {
158
157
  const response = await handler.callback.apply(handler.scope, [context]);
159
158
  if (!context.response.headersSent) {
160
- context.respondWith(response);
159
+ await context.respondWith(response);
161
160
  }
162
161
  }
163
162
  catch (e) {
@@ -175,13 +174,14 @@ export class Request {
175
174
  if (existsSync(assetPath)) {
176
175
  await this.app.emit('beforeAssetAccess', context);
177
176
  const extension = (context.request.url || '').split('.').pop();
177
+ let contentType = 'application/javascript';
178
178
  if (extension) {
179
- const contentType = this.app.contentType(extension);
180
- if (contentType) {
181
- response.setHeader('Content-Type', contentType);
179
+ const typeByExtension = this.app.contentType(extension);
180
+ if (typeByExtension) {
181
+ contentType = typeByExtension;
182
182
  }
183
183
  }
184
- response.write(readFileSync(assetPath));
184
+ this.sendResponse(request, response, readFileSync(assetPath), contentType);
185
185
  staticAsset = true;
186
186
  await this.app.emit('afterAssetAccess', context);
187
187
  }
@@ -395,4 +395,29 @@ export class Request {
395
395
  });
396
396
  return files;
397
397
  }
398
+ sendResponse(request, response, buffer, contentType) {
399
+ const mimeType = contentType.split(';')[0];
400
+ const gzipResponse = this.app.config.gzip.enabled &&
401
+ this.app.config.gzip.types.includes(mimeType) &&
402
+ request.headers['accept-encoding']?.includes('gzip') &&
403
+ buffer.length >= this.app.config.gzip.minSize;
404
+ if (!response.hasHeader('Content-Type')) {
405
+ response.setHeader('Content-Type', contentType);
406
+ }
407
+ if (typeof buffer === 'string') {
408
+ buffer = Buffer.from(buffer, 'utf-8');
409
+ }
410
+ if (gzipResponse) {
411
+ response.setHeader('Content-Encoding', 'gzip');
412
+ const compressed = zlib.gzipSync(buffer, {
413
+ level: this.app.config.gzip.compressionLevel
414
+ });
415
+ response.setHeader('Content-Length', compressed.length);
416
+ response.write(compressed);
417
+ }
418
+ else {
419
+ response.setHeader('Content-Length', buffer.length);
420
+ response.write(buffer);
421
+ }
422
+ }
398
423
  }
@@ -24,7 +24,7 @@ export type RequestContext<Body extends LooseObject | undefined = LooseObject> =
24
24
  isAjax: boolean;
25
25
  getArgs: PostedDataDecoded;
26
26
  timeStart: number;
27
- respondWith: (data: any) => void;
27
+ respondWith: (data: any) => Promise<void>;
28
28
  redirect: (to: string, statusCode?: number) => void;
29
29
  show404: () => Promise<void>;
30
30
  };
@@ -24,6 +24,12 @@ export type StructuredConfig = {
24
24
  port: number;
25
25
  linkHeaderRel: 'preload' | 'preconnect';
26
26
  };
27
+ gzip: {
28
+ enabled: boolean;
29
+ types: Array<string>;
30
+ minSize: number;
31
+ compressionLevel: number;
32
+ };
27
33
  readonly runtime: 'Node.js' | 'Deno';
28
34
  };
29
35
  export type StructuredClientConfig = {
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "type": "module",
21
21
  "main": "build/index",
22
- "version": "1.2.2",
22
+ "version": "1.3.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",