zuby 1.0.40 → 1.0.41

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ZUBY_BUILD_CHUNKS_MANIFEST, ZUBY_CONFIG_FILE } from './constants.js';
1
+ import { BUILD_CHUNKS_MANIFEST, ZUBY_CONFIG_FILE } from './constants.js';
2
2
  import { existsSync } from 'fs';
3
3
  import { bundleRequire } from 'bundle-require';
4
4
  import { createLogger } from './logger/index.js';
@@ -112,7 +112,7 @@ export const mergeDefaultConfig = async (config) => {
112
112
  config.vite.customLogger = config.vite.customLogger ?? config.customLogger;
113
113
  config.vite.server = config.vite.server ?? config.server;
114
114
  config.vite.publicDir = config.vite.publicDir ?? config.publicDir;
115
- config.vite.build.ssrManifest = ZUBY_BUILD_CHUNKS_MANIFEST;
115
+ config.vite.build.ssrManifest = BUILD_CHUNKS_MANIFEST;
116
116
  config.vite.build.ssrEmitAssets = config.vite.build.ssrEmitAssets ?? true;
117
117
  config.vite.build.modulePreload = { polyfill: false };
118
118
  config.vite.optimizeDeps = config.vite.optimizeDeps ?? {};
@@ -133,5 +133,11 @@ export const mergeDefaultConfig = async (config) => {
133
133
  * before removing any of them.
134
134
  */
135
135
  export const getBuiltInPlugins = () => {
136
- return [contextPlugin(), compileTimePlugin(), chunkNamingPlugin(), manifestPlugin(), prerenderPlugin()];
136
+ return [
137
+ contextPlugin(),
138
+ compileTimePlugin(),
139
+ chunkNamingPlugin(),
140
+ manifestPlugin(),
141
+ prerenderPlugin(),
142
+ ];
137
143
  };
package/constants.d.ts CHANGED
@@ -1,5 +1,25 @@
1
1
  export declare const ZUBY_CONFIG_FILE = "zuby.config.mjs";
2
- export declare const ZUBY_BUILD_CHUNKS_MANIFEST = "chunks-manifest.json";
3
- export declare const ZUBY_CLIENT_CHUNKS_MANIFEST = "client-chunks-manifest.json";
4
- export declare const ZUBY_SERVER_CHUNKS_MANIFEST = "server-chunks-manifest.json";
5
- export declare const ZUBY_PAGES_MANIFEST = "pages-manifest.json";
2
+ export declare const BUILD_CHUNKS_MANIFEST = "chunks-manifest.json";
3
+ export declare const CLIENT_CHUNKS_MANIFEST = "client-chunks-manifest.json";
4
+ export declare const SERVER_CHUNKS_MANIFEST = "server-chunks-manifest.json";
5
+ export declare const PAGES_MANIFEST = "pages-manifest.json";
6
+ export declare const HTTP_HEADERS: {
7
+ Age: string;
8
+ Host: string;
9
+ Server: string;
10
+ ContentType: string;
11
+ ContentLength: string;
12
+ ContentRange: string;
13
+ ContentEncoding: string;
14
+ ContentLanguage: string;
15
+ AcceptRanges: string;
16
+ CacheControl: string;
17
+ XForwardedProto: string;
18
+ XForwardedHost: string;
19
+ XPoweredBy: string;
20
+ XZubyVersion: string;
21
+ XZubyProps: string;
22
+ XZubyCachedTime: string;
23
+ XZubyCacheTTL: string;
24
+ XZubyCache: string;
25
+ };
package/constants.js CHANGED
@@ -1,5 +1,28 @@
1
1
  export const ZUBY_CONFIG_FILE = 'zuby.config.mjs';
2
- export const ZUBY_BUILD_CHUNKS_MANIFEST = 'chunks-manifest.json';
3
- export const ZUBY_CLIENT_CHUNKS_MANIFEST = 'client-chunks-manifest.json';
4
- export const ZUBY_SERVER_CHUNKS_MANIFEST = 'server-chunks-manifest.json';
5
- export const ZUBY_PAGES_MANIFEST = 'pages-manifest.json';
2
+ export const BUILD_CHUNKS_MANIFEST = 'chunks-manifest.json';
3
+ export const CLIENT_CHUNKS_MANIFEST = 'client-chunks-manifest.json';
4
+ export const SERVER_CHUNKS_MANIFEST = 'server-chunks-manifest.json';
5
+ export const PAGES_MANIFEST = 'pages-manifest.json';
6
+ export const HTTP_HEADERS = {
7
+ // Standard HTTP headers
8
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
9
+ Age: 'Age',
10
+ Host: 'Host',
11
+ Server: 'Server',
12
+ ContentType: 'Content-Type',
13
+ ContentLength: 'Content-Length',
14
+ ContentRange: 'Content-Range',
15
+ ContentEncoding: 'Content-Encoding',
16
+ ContentLanguage: 'Content-Language',
17
+ AcceptRanges: 'Accept-Ranges',
18
+ CacheControl: 'Cache-Control',
19
+ XForwardedProto: 'X-Forwarded-Proto',
20
+ XForwardedHost: 'X-Forwarded-Host',
21
+ XPoweredBy: 'X-Powered-By',
22
+ // Zuby specific
23
+ XZubyVersion: 'X-Zuby-Version',
24
+ XZubyProps: 'X-Zuby-Props',
25
+ XZubyCachedTime: 'X-Zuby-Cached-Time',
26
+ XZubyCacheTTL: 'X-Zuby-Cache-TTL',
27
+ XZubyCache: 'X-Zuby-Cache',
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "Zuby.js is framework for building SPA apps using Vite",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -6,6 +6,8 @@ export declare class ZubyPageContext {
6
6
  protected _params: Record<string, string>;
7
7
  protected _clientAddress?: string;
8
8
  protected _statusCode: number;
9
+ protected _headers: Headers;
10
+ protected _cache: number;
9
11
  protected _props: Record<string, any>;
10
12
  protected _zubyContext: ZubyContext;
11
13
  constructor(options: {
@@ -15,6 +17,7 @@ export declare class ZubyPageContext {
15
17
  clientAddress?: string;
16
18
  statusCode?: number;
17
19
  props?: Record<string, any>;
20
+ headers?: Headers;
18
21
  zubyContext?: ZubyContext;
19
22
  });
20
23
  get url(): URL;
@@ -40,4 +43,7 @@ export declare class ZubyPageContext {
40
43
  set props(value: Record<string, any>);
41
44
  get statusCode(): number | undefined;
42
45
  set statusCode(value: number | undefined);
46
+ get headers(): Headers;
47
+ get cache(): number;
48
+ set cache(value: number);
43
49
  }
@@ -1,13 +1,15 @@
1
1
  import { getContext } from '../context/index.js';
2
2
  export class ZubyPageContext {
3
3
  constructor(options) {
4
- this._url = options?.url || new URL('http://localhost/');
5
- this._title = options?.title || '';
6
4
  this._request = options?.request;
5
+ this._url = options?.url || new URL(options?.request?.url || '/', 'http://localhost/');
6
+ this._title = options?.title || '';
7
7
  this._params = {};
8
8
  this._clientAddress = options?.clientAddress;
9
9
  this._statusCode = options?.statusCode || 200;
10
10
  this._props = options?.props || {};
11
+ this._cache = 0;
12
+ this._headers = options?.headers || new Headers();
11
13
  this._zubyContext = options?.zubyContext || getContext();
12
14
  }
13
15
  get url() {
@@ -70,4 +72,16 @@ export class ZubyPageContext {
70
72
  }
71
73
  this._statusCode = value;
72
74
  }
75
+ get headers() {
76
+ return this._headers;
77
+ }
78
+ get cache() {
79
+ return this._cache;
80
+ }
81
+ set cache(value) {
82
+ if (!Number.isInteger(value) || value < 0) {
83
+ throw new Error(`Invalid cache value: ${value}`);
84
+ }
85
+ this._cache = value;
86
+ }
73
87
  }
@@ -1,7 +1,7 @@
1
1
  import { renameSync } from 'fs';
2
2
  import { normalizePath } from '../../utils/pathUtils.js';
3
3
  import { join } from 'path';
4
- import { ZUBY_BUILD_CHUNKS_MANIFEST, ZUBY_CLIENT_CHUNKS_MANIFEST, ZUBY_SERVER_CHUNKS_MANIFEST, } from '../../constants.js';
4
+ import { BUILD_CHUNKS_MANIFEST, CLIENT_CHUNKS_MANIFEST, SERVER_CHUNKS_MANIFEST, } from '../../constants.js';
5
5
  /**
6
6
  * This is internal plugin
7
7
  * which generated and moves required manifest files.
@@ -21,8 +21,8 @@ export default function manifestPlugin() {
21
21
  return;
22
22
  }
23
23
  // Move chunks manifest files
24
- renameSync(normalizePath(join(viteConfig.build.outDir, '..', 'client', ZUBY_BUILD_CHUNKS_MANIFEST)), normalizePath(join(viteConfig.build.outDir, '..', ZUBY_CLIENT_CHUNKS_MANIFEST)));
25
- renameSync(normalizePath(join(viteConfig.build.outDir, '..', 'server', ZUBY_BUILD_CHUNKS_MANIFEST)), normalizePath(join(viteConfig.build.outDir, '..', ZUBY_SERVER_CHUNKS_MANIFEST)));
26
- }
24
+ renameSync(normalizePath(join(viteConfig.build.outDir, '..', 'client', BUILD_CHUNKS_MANIFEST)), normalizePath(join(viteConfig.build.outDir, '..', CLIENT_CHUNKS_MANIFEST)));
25
+ renameSync(normalizePath(join(viteConfig.build.outDir, '..', 'server', BUILD_CHUNKS_MANIFEST)), normalizePath(join(viteConfig.build.outDir, '..', SERVER_CHUNKS_MANIFEST)));
26
+ },
27
27
  };
28
28
  }
@@ -10,7 +10,7 @@ import { OUTPUTS } from '../../types.js';
10
10
  import { PATH_TYPES } from '../../templates/types.js';
11
11
  import { substitutePathParams } from '../../templates/pathUtils.js';
12
12
  import { normalizePath } from '../../utils/pathUtils.js';
13
- import { ZUBY_SERVER_CHUNKS_MANIFEST } from '../../constants.js';
13
+ import { SERVER_CHUNKS_MANIFEST } from '../../constants.js';
14
14
  /**
15
15
  * This is internal plugin
16
16
  * that pre-renders the pages during the build.
@@ -31,7 +31,7 @@ export default function prerenderPlugin() {
31
31
  }
32
32
  const startTime = performance.now();
33
33
  const { srcDir, outDir, customLogger, prerenderPaths, site, output } = await getZubyInternalConfig();
34
- const chunksManifestPath = resolve(outDir, ZUBY_SERVER_CHUNKS_MANIFEST);
34
+ const chunksManifestPath = resolve(outDir, SERVER_CHUNKS_MANIFEST);
35
35
  const chunksManifest = existsSync(chunksManifestPath)
36
36
  ? JSON.parse(readFileSync(chunksManifestPath, 'utf-8'))
37
37
  : {};
package/server/index.js CHANGED
@@ -2099,13 +2099,15 @@ var getContext = () => {
2099
2099
  // src/pageContext/index.ts
2100
2100
  var ZubyPageContext = class {
2101
2101
  constructor(options) {
2102
- this._url = options?.url || new URL("http://localhost/");
2103
- this._title = options?.title || "";
2104
2102
  this._request = options?.request;
2103
+ this._url = options?.url || new URL(options?.request?.url || "/", "http://localhost/");
2104
+ this._title = options?.title || "";
2105
2105
  this._params = {};
2106
2106
  this._clientAddress = options?.clientAddress;
2107
2107
  this._statusCode = options?.statusCode || 200;
2108
2108
  this._props = options?.props || {};
2109
+ this._cache = 0;
2110
+ this._headers = options?.headers || new Headers();
2109
2111
  this._zubyContext = options?.zubyContext || getContext();
2110
2112
  }
2111
2113
  get url() {
@@ -2168,6 +2170,18 @@ var ZubyPageContext = class {
2168
2170
  }
2169
2171
  this._statusCode = value;
2170
2172
  }
2173
+ get headers() {
2174
+ return this._headers;
2175
+ }
2176
+ get cache() {
2177
+ return this._cache;
2178
+ }
2179
+ set cache(value) {
2180
+ if (!Number.isInteger(value) || value < 0) {
2181
+ throw new Error(`Invalid cache value: ${value}`);
2182
+ }
2183
+ this._cache = value;
2184
+ }
2171
2185
  };
2172
2186
 
2173
2187
  // src/server/zubyRenderer.ts
@@ -8390,7 +8404,30 @@ function findMatchingTemplate(templates, path2) {
8390
8404
  }
8391
8405
 
8392
8406
  // src/constants.ts
8393
- var ZUBY_CLIENT_CHUNKS_MANIFEST = "client-chunks-manifest.json";
8407
+ var CLIENT_CHUNKS_MANIFEST = "client-chunks-manifest.json";
8408
+ var HTTP_HEADERS = {
8409
+ // Standard HTTP headers
8410
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
8411
+ Age: "Age",
8412
+ Host: "Host",
8413
+ Server: "Server",
8414
+ ContentType: "Content-Type",
8415
+ ContentLength: "Content-Length",
8416
+ ContentRange: "Content-Range",
8417
+ ContentEncoding: "Content-Encoding",
8418
+ ContentLanguage: "Content-Language",
8419
+ AcceptRanges: "Accept-Ranges",
8420
+ CacheControl: "Cache-Control",
8421
+ XForwardedProto: "X-Forwarded-Proto",
8422
+ XForwardedHost: "X-Forwarded-Host",
8423
+ XPoweredBy: "X-Powered-By",
8424
+ // Zuby specific
8425
+ XZubyVersion: "X-Zuby-Version",
8426
+ XZubyProps: "X-Zuby-Props",
8427
+ XZubyCachedTime: "X-Zuby-Cached-Time",
8428
+ XZubyCacheTTL: "X-Zuby-Cache-TTL",
8429
+ XZubyCache: "X-Zuby-Cache"
8430
+ };
8394
8431
 
8395
8432
  // src/server/zubyRenderer.ts
8396
8433
  var ZubyRenderer = class {
@@ -8412,7 +8449,7 @@ var ZubyRenderer = class {
8412
8449
  if (this.entryClientCss) {
8413
8450
  this.entryClientCss = `/chunks/${basename(this.entryClientCss || "")}`;
8414
8451
  }
8415
- const manifestPath = resolve2(this.outDir, ZUBY_CLIENT_CHUNKS_MANIFEST);
8452
+ const manifestPath = resolve2(this.outDir, CLIENT_CHUNKS_MANIFEST);
8416
8453
  if (existsSync(manifestPath)) {
8417
8454
  this.chunksManifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
8418
8455
  }
@@ -8440,33 +8477,42 @@ var ZubyRenderer = class {
8440
8477
  if (!handlerResult)
8441
8478
  return;
8442
8479
  if (handlerResult instanceof Response) {
8443
- return handlerResult;
8480
+ return this.injectHeaders(handlerResult, pageContext);
8444
8481
  }
8445
8482
  if (typeof handlerResult === "string") {
8446
- return new Response(handlerResult.toString(), {
8483
+ return this.injectHeaders(
8484
+ new Response(handlerResult.toString(), {
8485
+ status: pageContext.statusCode || 200,
8486
+ headers: {
8487
+ "Content-Type": "text/html;charset=UTF-8"
8488
+ }
8489
+ }),
8490
+ pageContext
8491
+ );
8492
+ }
8493
+ return this.injectHeaders(
8494
+ new Response(JSON.stringify(handlerResult, null, 2), {
8447
8495
  status: pageContext.statusCode || 200,
8448
8496
  headers: {
8449
- "Content-Type": "text/html;charset=UTF-8"
8497
+ "Content-Type": "application/json;charset=UTF-8"
8450
8498
  }
8451
- });
8452
- }
8453
- return new Response(JSON.stringify(handlerResult, null, 2), {
8454
- status: pageContext.statusCode || 200,
8455
- headers: {
8456
- "Content-Type": "application/json;charset=UTF-8"
8457
- }
8458
- });
8499
+ }),
8500
+ pageContext
8501
+ );
8459
8502
  }
8460
8503
  async executeProps(pageContext) {
8461
- const propsHeader = pageContext.request?.headers.get("x-zuby-props");
8504
+ const propsHeader = pageContext.request?.headers.get(HTTP_HEADERS.XZubyProps);
8462
8505
  if (propsHeader !== "true")
8463
8506
  return;
8464
- return new Response(JSON.stringify(pageContext.props, null, 2), {
8465
- status: pageContext.statusCode || 200,
8466
- headers: {
8467
- "Content-Type": "application/json"
8468
- }
8469
- });
8507
+ return this.injectHeaders(
8508
+ new Response(JSON.stringify(pageContext.props, null, 2), {
8509
+ status: pageContext.statusCode || 200,
8510
+ headers: {
8511
+ "Content-Type": "application/json"
8512
+ }
8513
+ }),
8514
+ pageContext
8515
+ );
8470
8516
  }
8471
8517
  async executeEntry(pageContext) {
8472
8518
  const pages = this.zubyContext?.templates?.pages || [];
@@ -8494,7 +8540,8 @@ var ZubyRenderer = class {
8494
8540
  ) || "";
8495
8541
  const layoutChildren = layoutComponent({
8496
8542
  children: innerLayoutComponent({
8497
- innerHtml: entryHtml
8543
+ innerHtml: entryHtml,
8544
+ context: pageContext
8498
8545
  }),
8499
8546
  context: pageContext
8500
8547
  });
@@ -8518,42 +8565,59 @@ var ZubyRenderer = class {
8518
8565
  jsImports.forEach((imp) => {
8519
8566
  html = html.replace(/(<\/head>)/, `<script type="module" src="${imp}" defer></script>$1`);
8520
8567
  });
8521
- return new Response(html, {
8522
- status: pageContext.statusCode || 200,
8523
- headers: {
8524
- "Content-Type": "text/html;charset=UTF-8"
8525
- }
8526
- });
8568
+ return this.injectHeaders(
8569
+ new Response(html, {
8570
+ status: pageContext.statusCode || 200,
8571
+ headers: {
8572
+ "Content-Type": "text/html;charset=UTF-8"
8573
+ }
8574
+ }),
8575
+ pageContext
8576
+ );
8527
8577
  }
8528
8578
  createPageContext(req) {
8529
8579
  const pageUrl = new URL(req.url);
8530
8580
  if (pageUrl.pathname.startsWith("/_props/")) {
8531
8581
  pageUrl.pathname = pageUrl.pathname.replace(/^\/_props\//, "/");
8532
- req.headers.set("x-zuby-props", "true");
8582
+ req.headers.set(HTTP_HEADERS.XZubyProps, "true");
8533
8583
  }
8534
8584
  return new ZubyPageContext({
8535
8585
  url: pageUrl,
8536
8586
  request: req,
8537
8587
  statusCode: 200,
8538
8588
  props: {},
8539
- zubyContext: this.zubyContext
8589
+ zubyContext: this.zubyContext,
8590
+ headers: new Headers({
8591
+ [HTTP_HEADERS.XPoweredBy]: "Zuby.js"
8592
+ })
8540
8593
  });
8541
8594
  }
8542
8595
  async render(req) {
8543
8596
  const pageContext = this.createPageContext(req);
8544
8597
  return await this.executeHandlers(pageContext) || await this.executeProps(pageContext) || await this.executeEntry(pageContext);
8545
8598
  }
8599
+ injectHeaders(res, pageContext) {
8600
+ pageContext.headers.forEach((value, key) => {
8601
+ if (res.headers.has(key))
8602
+ return;
8603
+ res.headers.set(key, value);
8604
+ });
8605
+ if (pageContext.cache) {
8606
+ res.headers.set(HTTP_HEADERS.XZubyCacheTTL, pageContext.cache.toString());
8607
+ }
8608
+ return res;
8609
+ }
8546
8610
  };
8547
8611
 
8548
8612
  // src/server/zubyServer.ts
8549
8613
  var ZubyServer = class {
8550
8614
  constructor(options) {
8615
+ this.cache = /* @__PURE__ */ new Map();
8551
8616
  this.options = options;
8552
8617
  this.options.responseHeaders = this.options.responseHeaders || {};
8553
8618
  this.logger = options.logger || createLogger("info");
8554
8619
  this.renderer = options.renderer || new ZubyRenderer(options.outDir);
8555
8620
  this.middlewares = [
8556
- this.defaultHeadersMiddleware.bind(this),
8557
8621
  this.trailingSlashMiddleware.bind(this),
8558
8622
  this.clientDirMiddleware.bind(this),
8559
8623
  this.serverDirMiddleware.bind(this)
@@ -8633,11 +8697,38 @@ var ZubyServer = class {
8633
8697
  res.end();
8634
8698
  });
8635
8699
  }
8636
- async serverDirMiddleware(nodeReq, nodeRes) {
8700
+ async serverDirMiddleware(nodeReq, nodeRes, _next) {
8637
8701
  const req = await this.toRequest(nodeReq);
8638
- const res = await this.renderer.render(req);
8702
+ const cacheKey = new URL(req.url, "http://localhost").pathname;
8703
+ const res = this.cacheRead(cacheKey) || this.cacheWrite(cacheKey, await this.renderer.render(req));
8639
8704
  return this.toNodeResponse(res, nodeRes);
8640
8705
  }
8706
+ cacheRead(key) {
8707
+ const cachedResponse = this.cache.get(key)?.clone();
8708
+ if (!cachedResponse)
8709
+ return void 0;
8710
+ const cachedTime = Number(cachedResponse.headers.get(HTTP_HEADERS.XZubyCachedTime));
8711
+ const cacheTTL = Number(cachedResponse.headers.get(HTTP_HEADERS.XZubyCacheTTL));
8712
+ const age = Math.ceil(((/* @__PURE__ */ new Date()).getTime() - cachedTime) / 1e3);
8713
+ if (age > cacheTTL) {
8714
+ this.cache.delete(key);
8715
+ return void 0;
8716
+ }
8717
+ cachedResponse.headers.set(HTTP_HEADERS.Age, age.toString());
8718
+ cachedResponse.headers.set(HTTP_HEADERS.XZubyCache, "HIT");
8719
+ return cachedResponse;
8720
+ }
8721
+ cacheWrite(key, response) {
8722
+ const cacheTTL = Number(response.headers.get(HTTP_HEADERS.XZubyCacheTTL));
8723
+ if (!cacheTTL) {
8724
+ response.headers.set(HTTP_HEADERS.XZubyCache, "BYPASS");
8725
+ return response;
8726
+ }
8727
+ response.headers.set(HTTP_HEADERS.XZubyCachedTime, (/* @__PURE__ */ new Date()).getTime().toString());
8728
+ response.headers.set(HTTP_HEADERS.XZubyCache, "MISS");
8729
+ this.cache.set(key, response.clone());
8730
+ return response;
8731
+ }
8641
8732
  async listen() {
8642
8733
  const serverPromise = new Promise((resolve5, reject) => {
8643
8734
  this.server.listen(this.options.port, this.options.host, 511, () => {
@@ -8659,10 +8750,6 @@ var ZubyServer = class {
8659
8750
  });
8660
8751
  });
8661
8752
  }
8662
- defaultHeadersMiddleware(_req, res, next) {
8663
- res.setHeader("X-Powered-By", "Zuby.js");
8664
- next();
8665
- }
8666
8753
  trailingSlashMiddleware(req, res, next) {
8667
8754
  const [path2, query] = req.url?.split("?") || [];
8668
8755
  if (path2.endsWith("/") || path2.match(/[.](.+)$/)) {
@@ -3,6 +3,7 @@ import { relative } from 'path';
3
3
  import { getContext } from '../context/index.js';
4
4
  import ZubyRenderer from './zubyRenderer.js';
5
5
  import { getEntryFile } from '../commands/build.js';
6
+ import { HTTP_HEADERS } from '../constants.js';
6
7
  export default class ZubyDevRenderer extends ZubyRenderer {
7
8
  constructor(zubyConfig, viteServerDevServer) {
8
9
  super(zubyConfig.outDir);
@@ -23,6 +24,9 @@ export default class ZubyDevRenderer extends ZubyRenderer {
23
24
  }
24
25
  async render(req) {
25
26
  const originalRes = await super.render(req);
27
+ // Do not cache responses in dev mode
28
+ originalRes.headers.set(HTTP_HEADERS.XZubyCacheTTL, '0');
29
+ originalRes.headers.set(HTTP_HEADERS.CacheControl, 'no-store, no-cache, must-revalidate');
26
30
  // Do not transform non-html responses
27
31
  if (!originalRes.headers.get('content-type')?.includes('text/html'))
28
32
  return originalRes;
@@ -15,7 +15,6 @@ export default class ZubyDevServer extends ZubyServer {
15
15
  });
16
16
  this.zubyInternalConfig = zubyConfig;
17
17
  this.middlewares = [
18
- this.defaultHeadersMiddleware.bind(this),
19
18
  this.trailingSlashMiddleware.bind(this),
20
19
  this.serverDirMiddleware.bind(this),
21
20
  ];
@@ -21,4 +21,5 @@ export default class ZubyRenderer {
21
21
  executeEntry(pageContext: ZubyPageContext): Promise<Response>;
22
22
  createPageContext(req: Request): ZubyPageContext;
23
23
  render(req: Request): Promise<Response>;
24
+ injectHeaders(res: Response, pageContext: ZubyPageContext): Response;
24
25
  }
@@ -5,7 +5,7 @@ import { basename, resolve } from 'path';
5
5
  import { glob } from 'glob';
6
6
  import { existsSync, readFileSync } from 'fs';
7
7
  import { findMatchingTemplate } from '../templates/pathUtils.js';
8
- import { ZUBY_CLIENT_CHUNKS_MANIFEST } from '../constants.js';
8
+ import { CLIENT_CHUNKS_MANIFEST, HTTP_HEADERS } from '../constants.js';
9
9
  export default class ZubyRenderer {
10
10
  constructor(outDir) {
11
11
  this.outDir = outDir;
@@ -25,7 +25,7 @@ export default class ZubyRenderer {
25
25
  if (this.entryClientCss) {
26
26
  this.entryClientCss = `/chunks/${basename(this.entryClientCss || '')}`;
27
27
  }
28
- const manifestPath = resolve(this.outDir, ZUBY_CLIENT_CHUNKS_MANIFEST);
28
+ const manifestPath = resolve(this.outDir, CLIENT_CHUNKS_MANIFEST);
29
29
  if (existsSync(manifestPath)) {
30
30
  this.chunksManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
31
31
  }
@@ -57,36 +57,36 @@ export default class ZubyRenderer {
57
57
  return;
58
58
  // If handler returned a response, we can immediately return it
59
59
  if (handlerResult instanceof Response) {
60
- return handlerResult;
60
+ return this.injectHeaders(handlerResult, pageContext);
61
61
  }
62
62
  // If handler returned string,
63
63
  // we'll assume that user wants to return html response
64
64
  if (typeof handlerResult === 'string') {
65
- return new Response(handlerResult.toString(), {
65
+ return this.injectHeaders(new Response(handlerResult.toString(), {
66
66
  status: pageContext.statusCode || 200,
67
67
  headers: {
68
68
  'Content-Type': 'text/html;charset=UTF-8',
69
69
  },
70
- });
70
+ }), pageContext);
71
71
  }
72
72
  // We'll try to serialize everything else as JSON
73
- return new Response(JSON.stringify(handlerResult, null, 2), {
73
+ return this.injectHeaders(new Response(JSON.stringify(handlerResult, null, 2), {
74
74
  status: pageContext.statusCode || 200,
75
75
  headers: {
76
76
  'Content-Type': 'application/json;charset=UTF-8',
77
77
  },
78
- });
78
+ }), pageContext);
79
79
  }
80
80
  async executeProps(pageContext) {
81
- const propsHeader = pageContext.request?.headers.get('x-zuby-props');
81
+ const propsHeader = pageContext.request?.headers.get(HTTP_HEADERS.XZubyProps);
82
82
  if (propsHeader !== 'true')
83
83
  return;
84
- return new Response(JSON.stringify(pageContext.props, null, 2), {
84
+ return this.injectHeaders(new Response(JSON.stringify(pageContext.props, null, 2), {
85
85
  status: pageContext.statusCode || 200,
86
86
  headers: {
87
87
  'Content-Type': 'application/json',
88
88
  },
89
- });
89
+ }), pageContext);
90
90
  }
91
91
  async executeEntry(pageContext) {
92
92
  // Load page template component
@@ -119,6 +119,7 @@ export default class ZubyRenderer {
119
119
  const layoutChildren = layoutComponent({
120
120
  children: innerLayoutComponent({
121
121
  innerHtml: entryHtml,
122
+ context: pageContext,
122
123
  }),
123
124
  context: pageContext,
124
125
  });
@@ -148,12 +149,12 @@ export default class ZubyRenderer {
148
149
  jsImports.forEach((imp) => {
149
150
  html = html.replace(/(<\/head>)/, `<script type="module" src="${imp}" defer></script>$1`);
150
151
  });
151
- return new Response(html, {
152
+ return this.injectHeaders(new Response(html, {
152
153
  status: pageContext.statusCode || 200,
153
154
  headers: {
154
155
  'Content-Type': 'text/html;charset=UTF-8',
155
156
  },
156
- });
157
+ }), pageContext);
157
158
  }
158
159
  createPageContext(req) {
159
160
  // Paths starting with /_props are special case.
@@ -162,7 +163,7 @@ export default class ZubyRenderer {
162
163
  const pageUrl = new URL(req.url);
163
164
  if (pageUrl.pathname.startsWith('/_props/')) {
164
165
  pageUrl.pathname = pageUrl.pathname.replace(/^\/_props\//, '/');
165
- req.headers.set('x-zuby-props', 'true');
166
+ req.headers.set(HTTP_HEADERS.XZubyProps, 'true');
166
167
  }
167
168
  return new ZubyPageContext({
168
169
  url: pageUrl,
@@ -170,6 +171,9 @@ export default class ZubyRenderer {
170
171
  statusCode: 200,
171
172
  props: {},
172
173
  zubyContext: this.zubyContext,
174
+ headers: new Headers({
175
+ [HTTP_HEADERS.XPoweredBy]: 'Zuby.js',
176
+ }),
173
177
  });
174
178
  }
175
179
  async render(req) {
@@ -178,4 +182,15 @@ export default class ZubyRenderer {
178
182
  (await this.executeProps(pageContext)) ||
179
183
  (await this.executeEntry(pageContext)));
180
184
  }
185
+ injectHeaders(res, pageContext) {
186
+ pageContext.headers.forEach((value, key) => {
187
+ if (res.headers.has(key))
188
+ return;
189
+ res.headers.set(key, value);
190
+ });
191
+ if (pageContext.cache) {
192
+ res.headers.set(HTTP_HEADERS.XZubyCacheTTL, pageContext.cache.toString());
193
+ }
194
+ return res;
195
+ }
181
196
  }
@@ -11,14 +11,16 @@ export default class ZubyServer {
11
11
  protected logger: ZubyLogger;
12
12
  protected renderer: ZubyRenderer;
13
13
  protected middlewares: ZubyMiddleware[];
14
+ protected cache: Map<string, Response>;
14
15
  constructor(options: ZubyServerOptions);
15
16
  handle(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
16
- clientDirMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): Promise<void | NodeResponse>;
17
- serverDirMiddleware(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
17
+ clientDirMiddleware(req: NodeRequest, res: NodeResponse, next: () => any | Promise<any>): Promise<any>;
18
+ serverDirMiddleware(nodeReq: NodeRequest, nodeRes: NodeResponse, _next: () => any | Promise<any>): Promise<void>;
19
+ cacheRead(key: string): Response | undefined;
20
+ cacheWrite(key: string, response: Response): Response;
18
21
  listen(): Promise<void>;
19
22
  close(): Promise<void>;
20
- defaultHeadersMiddleware(_req: NodeRequest, res: NodeResponse, next: () => void): void;
21
- trailingSlashMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): void;
23
+ trailingSlashMiddleware(req: NodeRequest, res: NodeResponse, next: () => any | Promise<any>): any;
22
24
  toRequest(nodeReq: NodeRequest): Promise<Request>;
23
25
  toNodeResponse(res: Response, nodeRes: ServerResponse): Promise<void>;
24
26
  use(middleware: ZubyMiddleware): void;
@@ -7,14 +7,15 @@ import { createReadStream, existsSync, statSync } from 'fs';
7
7
  import { mimeTypes } from './mimeTypes.js';
8
8
  import { createLogger } from '../logger/index.js';
9
9
  import ZubyRenderer from './zubyRenderer.js';
10
+ import { HTTP_HEADERS } from '../constants.js';
10
11
  export default class ZubyServer {
11
12
  constructor(options) {
13
+ this.cache = new Map();
12
14
  this.options = options;
13
15
  this.options.responseHeaders = this.options.responseHeaders || {};
14
16
  this.logger = options.logger || createLogger('info');
15
17
  this.renderer = options.renderer || new ZubyRenderer(options.outDir);
16
18
  this.middlewares = [
17
- this.defaultHeadersMiddleware.bind(this),
18
19
  this.trailingSlashMiddleware.bind(this),
19
20
  this.clientDirMiddleware.bind(this),
20
21
  this.serverDirMiddleware.bind(this),
@@ -103,11 +104,38 @@ export default class ZubyServer {
103
104
  res.end();
104
105
  });
105
106
  }
106
- async serverDirMiddleware(nodeReq, nodeRes) {
107
+ async serverDirMiddleware(nodeReq, nodeRes, _next) {
107
108
  const req = await this.toRequest(nodeReq);
108
- const res = await this.renderer.render(req);
109
+ const cacheKey = new URL(req.url, 'http://localhost').pathname;
110
+ const res = this.cacheRead(cacheKey) || this.cacheWrite(cacheKey, await this.renderer.render(req));
109
111
  return this.toNodeResponse(res, nodeRes);
110
112
  }
113
+ cacheRead(key) {
114
+ const cachedResponse = this.cache.get(key)?.clone();
115
+ if (!cachedResponse)
116
+ return undefined;
117
+ const cachedTime = Number(cachedResponse.headers.get(HTTP_HEADERS.XZubyCachedTime));
118
+ const cacheTTL = Number(cachedResponse.headers.get(HTTP_HEADERS.XZubyCacheTTL));
119
+ const age = Math.ceil((new Date().getTime() - cachedTime) / 1000);
120
+ if (age > cacheTTL) {
121
+ this.cache.delete(key);
122
+ return undefined;
123
+ }
124
+ cachedResponse.headers.set(HTTP_HEADERS.Age, age.toString());
125
+ cachedResponse.headers.set(HTTP_HEADERS.XZubyCache, 'HIT');
126
+ return cachedResponse;
127
+ }
128
+ cacheWrite(key, response) {
129
+ const cacheTTL = Number(response.headers.get(HTTP_HEADERS.XZubyCacheTTL));
130
+ if (!cacheTTL) {
131
+ response.headers.set(HTTP_HEADERS.XZubyCache, 'BYPASS');
132
+ return response;
133
+ }
134
+ response.headers.set(HTTP_HEADERS.XZubyCachedTime, new Date().getTime().toString());
135
+ response.headers.set(HTTP_HEADERS.XZubyCache, 'MISS');
136
+ this.cache.set(key, response.clone());
137
+ return response;
138
+ }
111
139
  async listen() {
112
140
  const serverPromise = new Promise((resolve, reject) => {
113
141
  this.server.listen(this.options.port, this.options.host, 511, () => {
@@ -129,10 +157,6 @@ export default class ZubyServer {
129
157
  });
130
158
  });
131
159
  }
132
- defaultHeadersMiddleware(_req, res, next) {
133
- res.setHeader('X-Powered-By', 'Zuby.js');
134
- next();
135
- }
136
160
  trailingSlashMiddleware(req, res, next) {
137
161
  // Redirect to path with trailing slash for consistency
138
162
  const [path, query] = req.url?.split('?') || [];