zuby 1.0.39 → 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.d.ts CHANGED
@@ -30,4 +30,4 @@ export declare const mergeDefaultConfig: (config: ZubyConfig) => Promise<ZubyInt
30
30
  * Check which framework components are relying on each plugin
31
31
  * before removing any of them.
32
32
  */
33
- export declare const getBuiltInPlugins: () => import("vite").PluginOption[];
33
+ export declare const getBuiltInPlugins: () => (false | import("vite").Plugin<any> | import("vite").PluginOption[] | Promise<false | import("vite").Plugin<any> | import("vite").PluginOption[] | null | undefined> | null | undefined)[];
package/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { 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';
@@ -7,6 +7,7 @@ import contextPlugin from './plugins/contextPlugin/index.js';
7
7
  import compileTimePlugin from './plugins/compileTimePlugin/index.js';
8
8
  import chunkNamingPlugin from './plugins/chunkNamingPlugin/index.js';
9
9
  import prerenderPlugin from './plugins/prerenderPlugin/index.js';
10
+ import manifestPlugin from './plugins/manifestPlugin/index.js';
10
11
  let zubyInternalConfig;
11
12
  /**
12
13
  * Returns the path to the ZubyConfig file.
@@ -111,8 +112,9 @@ export const mergeDefaultConfig = async (config) => {
111
112
  config.vite.customLogger = config.vite.customLogger ?? config.customLogger;
112
113
  config.vite.server = config.vite.server ?? config.server;
113
114
  config.vite.publicDir = config.vite.publicDir ?? config.publicDir;
114
- config.vite.build.ssrManifest = config.vite.build.ssrManifest ?? 'chunks-manifest.json';
115
+ config.vite.build.ssrManifest = BUILD_CHUNKS_MANIFEST;
115
116
  config.vite.build.ssrEmitAssets = config.vite.build.ssrEmitAssets ?? true;
117
+ config.vite.build.modulePreload = { polyfill: false };
116
118
  config.vite.optimizeDeps = config.vite.optimizeDeps ?? {};
117
119
  // Merge built-in plugins with user plugins
118
120
  config.vite.plugins = [
@@ -131,5 +133,11 @@ export const mergeDefaultConfig = async (config) => {
131
133
  * before removing any of them.
132
134
  */
133
135
  export const getBuiltInPlugins = () => {
134
- return [contextPlugin(), compileTimePlugin(), chunkNamingPlugin(), prerenderPlugin()];
136
+ return [
137
+ contextPlugin(),
138
+ compileTimePlugin(),
139
+ chunkNamingPlugin(),
140
+ manifestPlugin(),
141
+ prerenderPlugin(),
142
+ ];
135
143
  };
package/constants.d.ts CHANGED
@@ -1 +1,25 @@
1
1
  export declare const ZUBY_CONFIG_FILE = "zuby.config.mjs";
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 +1,28 @@
1
1
  export const ZUBY_CONFIG_FILE = 'zuby.config.mjs';
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.39",
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",
@@ -17,21 +17,20 @@
17
17
  "linkDirectory": true
18
18
  },
19
19
  "dependencies": {
20
- "@vercel/nft": "^0.24.3",
20
+ "@vercel/nft": "^0.26.2",
21
21
  "bundle-require": "^4.0.2",
22
22
  "chalk": "^5.3.0",
23
23
  "commander": "^11.0.0",
24
24
  "devalue": "^4.3.2",
25
- "esbuild": "^0.19.5",
25
+ "esbuild": "^0.19.11",
26
26
  "glob": "^10.3.10",
27
- "inquirer": "^9.2.11",
28
- "jest": "^29.7.0",
27
+ "inquirer": "^9.2.12",
29
28
  "magic-string": "^0.30.5",
30
- "rollup": "^4.0.2",
31
- "vite": "^4.4.11"
29
+ "rollup": "^4.9.5",
30
+ "vite": "^5.0.11"
32
31
  },
33
32
  "devDependencies": {
34
- "@types/inquirer": "^9.0.6"
33
+ "@types/inquirer": "^9.0.7"
35
34
  },
36
35
  "bin": {
37
36
  "zuby": "./commands/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
  }
@@ -0,0 +1,6 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * This is internal plugin
4
+ * which generated and moves required manifest files.
5
+ */
6
+ export default function manifestPlugin(): Plugin;
@@ -0,0 +1,28 @@
1
+ import { renameSync } from 'fs';
2
+ import { normalizePath } from '../../utils/pathUtils.js';
3
+ import { join } from 'path';
4
+ import { BUILD_CHUNKS_MANIFEST, CLIENT_CHUNKS_MANIFEST, SERVER_CHUNKS_MANIFEST, } from '../../constants.js';
5
+ /**
6
+ * This is internal plugin
7
+ * which generated and moves required manifest files.
8
+ */
9
+ export default function manifestPlugin() {
10
+ let viteConfig;
11
+ return {
12
+ name: 'zuby-manifest-plugin',
13
+ apply: 'build',
14
+ enforce: 'post',
15
+ async configResolved(config) {
16
+ viteConfig = config;
17
+ },
18
+ async closeBundle() {
19
+ // If the build is not for SSR, then skip the plugin
20
+ if (!viteConfig?.build.ssr) {
21
+ return;
22
+ }
23
+ // Move chunks manifest files
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
+ };
28
+ }
@@ -10,6 +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 { SERVER_CHUNKS_MANIFEST } from '../../constants.js';
13
14
  /**
14
15
  * This is internal plugin
15
16
  * that pre-renders the pages during the build.
@@ -30,7 +31,7 @@ export default function prerenderPlugin() {
30
31
  }
31
32
  const startTime = performance.now();
32
33
  const { srcDir, outDir, customLogger, prerenderPaths, site, output } = await getZubyInternalConfig();
33
- const chunksManifestPath = resolve(outDir, 'server', 'chunks-manifest.json');
34
+ const chunksManifestPath = resolve(outDir, SERVER_CHUNKS_MANIFEST);
34
35
  const chunksManifest = existsSync(chunksManifestPath)
35
36
  ? JSON.parse(readFileSync(chunksManifestPath, 'utf-8'))
36
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
@@ -8389,6 +8403,32 @@ function findMatchingTemplate(templates, path2) {
8389
8403
  };
8390
8404
  }
8391
8405
 
8406
+ // src/constants.ts
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
+ };
8431
+
8392
8432
  // src/server/zubyRenderer.ts
8393
8433
  var ZubyRenderer = class {
8394
8434
  constructor(outDir) {
@@ -8409,7 +8449,7 @@ var ZubyRenderer = class {
8409
8449
  if (this.entryClientCss) {
8410
8450
  this.entryClientCss = `/chunks/${basename(this.entryClientCss || "")}`;
8411
8451
  }
8412
- const manifestPath = resolve2(this.outDir, "client", "chunks-manifest.json");
8452
+ const manifestPath = resolve2(this.outDir, CLIENT_CHUNKS_MANIFEST);
8413
8453
  if (existsSync(manifestPath)) {
8414
8454
  this.chunksManifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
8415
8455
  }
@@ -8437,33 +8477,42 @@ var ZubyRenderer = class {
8437
8477
  if (!handlerResult)
8438
8478
  return;
8439
8479
  if (handlerResult instanceof Response) {
8440
- return handlerResult;
8480
+ return this.injectHeaders(handlerResult, pageContext);
8441
8481
  }
8442
8482
  if (typeof handlerResult === "string") {
8443
- 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), {
8444
8495
  status: pageContext.statusCode || 200,
8445
8496
  headers: {
8446
- "Content-Type": "text/html;charset=UTF-8"
8497
+ "Content-Type": "application/json;charset=UTF-8"
8447
8498
  }
8448
- });
8449
- }
8450
- return new Response(JSON.stringify(handlerResult, null, 2), {
8451
- status: pageContext.statusCode || 200,
8452
- headers: {
8453
- "Content-Type": "application/json;charset=UTF-8"
8454
- }
8455
- });
8499
+ }),
8500
+ pageContext
8501
+ );
8456
8502
  }
8457
8503
  async executeProps(pageContext) {
8458
- const propsHeader = pageContext.request?.headers.get("x-zuby-props");
8504
+ const propsHeader = pageContext.request?.headers.get(HTTP_HEADERS.XZubyProps);
8459
8505
  if (propsHeader !== "true")
8460
8506
  return;
8461
- return new Response(JSON.stringify(pageContext.props, null, 2), {
8462
- status: pageContext.statusCode || 200,
8463
- headers: {
8464
- "Content-Type": "application/json"
8465
- }
8466
- });
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
+ );
8467
8516
  }
8468
8517
  async executeEntry(pageContext) {
8469
8518
  const pages = this.zubyContext?.templates?.pages || [];
@@ -8491,7 +8540,8 @@ var ZubyRenderer = class {
8491
8540
  ) || "";
8492
8541
  const layoutChildren = layoutComponent({
8493
8542
  children: innerLayoutComponent({
8494
- innerHtml: entryHtml
8543
+ innerHtml: entryHtml,
8544
+ context: pageContext
8495
8545
  }),
8496
8546
  context: pageContext
8497
8547
  });
@@ -8515,42 +8565,59 @@ var ZubyRenderer = class {
8515
8565
  jsImports.forEach((imp) => {
8516
8566
  html = html.replace(/(<\/head>)/, `<script type="module" src="${imp}" defer></script>$1`);
8517
8567
  });
8518
- return new Response(html, {
8519
- status: pageContext.statusCode || 200,
8520
- headers: {
8521
- "Content-Type": "text/html;charset=UTF-8"
8522
- }
8523
- });
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
+ );
8524
8577
  }
8525
8578
  createPageContext(req) {
8526
8579
  const pageUrl = new URL(req.url);
8527
8580
  if (pageUrl.pathname.startsWith("/_props/")) {
8528
8581
  pageUrl.pathname = pageUrl.pathname.replace(/^\/_props\//, "/");
8529
- req.headers.set("x-zuby-props", "true");
8582
+ req.headers.set(HTTP_HEADERS.XZubyProps, "true");
8530
8583
  }
8531
8584
  return new ZubyPageContext({
8532
8585
  url: pageUrl,
8533
8586
  request: req,
8534
8587
  statusCode: 200,
8535
8588
  props: {},
8536
- zubyContext: this.zubyContext
8589
+ zubyContext: this.zubyContext,
8590
+ headers: new Headers({
8591
+ [HTTP_HEADERS.XPoweredBy]: "Zuby.js"
8592
+ })
8537
8593
  });
8538
8594
  }
8539
8595
  async render(req) {
8540
8596
  const pageContext = this.createPageContext(req);
8541
8597
  return await this.executeHandlers(pageContext) || await this.executeProps(pageContext) || await this.executeEntry(pageContext);
8542
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
+ }
8543
8610
  };
8544
8611
 
8545
8612
  // src/server/zubyServer.ts
8546
8613
  var ZubyServer = class {
8547
8614
  constructor(options) {
8615
+ this.cache = /* @__PURE__ */ new Map();
8548
8616
  this.options = options;
8549
8617
  this.options.responseHeaders = this.options.responseHeaders || {};
8550
8618
  this.logger = options.logger || createLogger("info");
8551
8619
  this.renderer = options.renderer || new ZubyRenderer(options.outDir);
8552
8620
  this.middlewares = [
8553
- this.defaultHeadersMiddleware.bind(this),
8554
8621
  this.trailingSlashMiddleware.bind(this),
8555
8622
  this.clientDirMiddleware.bind(this),
8556
8623
  this.serverDirMiddleware.bind(this)
@@ -8630,11 +8697,38 @@ var ZubyServer = class {
8630
8697
  res.end();
8631
8698
  });
8632
8699
  }
8633
- async serverDirMiddleware(nodeReq, nodeRes) {
8700
+ async serverDirMiddleware(nodeReq, nodeRes, _next) {
8634
8701
  const req = await this.toRequest(nodeReq);
8635
- 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));
8636
8704
  return this.toNodeResponse(res, nodeRes);
8637
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
+ }
8638
8732
  async listen() {
8639
8733
  const serverPromise = new Promise((resolve5, reject) => {
8640
8734
  this.server.listen(this.options.port, this.options.host, 511, () => {
@@ -8656,10 +8750,6 @@ var ZubyServer = class {
8656
8750
  });
8657
8751
  });
8658
8752
  }
8659
- defaultHeadersMiddleware(_req, res, next) {
8660
- res.setHeader("X-Powered-By", "Zuby.js");
8661
- next();
8662
- }
8663
8753
  trailingSlashMiddleware(req, res, next) {
8664
8754
  const [path2, query] = req.url?.split("?") || [];
8665
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,6 +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 { CLIENT_CHUNKS_MANIFEST, HTTP_HEADERS } from '../constants.js';
8
9
  export default class ZubyRenderer {
9
10
  constructor(outDir) {
10
11
  this.outDir = outDir;
@@ -24,7 +25,7 @@ export default class ZubyRenderer {
24
25
  if (this.entryClientCss) {
25
26
  this.entryClientCss = `/chunks/${basename(this.entryClientCss || '')}`;
26
27
  }
27
- const manifestPath = resolve(this.outDir, 'client', 'chunks-manifest.json');
28
+ const manifestPath = resolve(this.outDir, CLIENT_CHUNKS_MANIFEST);
28
29
  if (existsSync(manifestPath)) {
29
30
  this.chunksManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
30
31
  }
@@ -56,36 +57,36 @@ export default class ZubyRenderer {
56
57
  return;
57
58
  // If handler returned a response, we can immediately return it
58
59
  if (handlerResult instanceof Response) {
59
- return handlerResult;
60
+ return this.injectHeaders(handlerResult, pageContext);
60
61
  }
61
62
  // If handler returned string,
62
63
  // we'll assume that user wants to return html response
63
64
  if (typeof handlerResult === 'string') {
64
- return new Response(handlerResult.toString(), {
65
+ return this.injectHeaders(new Response(handlerResult.toString(), {
65
66
  status: pageContext.statusCode || 200,
66
67
  headers: {
67
68
  'Content-Type': 'text/html;charset=UTF-8',
68
69
  },
69
- });
70
+ }), pageContext);
70
71
  }
71
72
  // We'll try to serialize everything else as JSON
72
- return new Response(JSON.stringify(handlerResult, null, 2), {
73
+ return this.injectHeaders(new Response(JSON.stringify(handlerResult, null, 2), {
73
74
  status: pageContext.statusCode || 200,
74
75
  headers: {
75
76
  'Content-Type': 'application/json;charset=UTF-8',
76
77
  },
77
- });
78
+ }), pageContext);
78
79
  }
79
80
  async executeProps(pageContext) {
80
- const propsHeader = pageContext.request?.headers.get('x-zuby-props');
81
+ const propsHeader = pageContext.request?.headers.get(HTTP_HEADERS.XZubyProps);
81
82
  if (propsHeader !== 'true')
82
83
  return;
83
- return new Response(JSON.stringify(pageContext.props, null, 2), {
84
+ return this.injectHeaders(new Response(JSON.stringify(pageContext.props, null, 2), {
84
85
  status: pageContext.statusCode || 200,
85
86
  headers: {
86
87
  'Content-Type': 'application/json',
87
88
  },
88
- });
89
+ }), pageContext);
89
90
  }
90
91
  async executeEntry(pageContext) {
91
92
  // Load page template component
@@ -118,6 +119,7 @@ export default class ZubyRenderer {
118
119
  const layoutChildren = layoutComponent({
119
120
  children: innerLayoutComponent({
120
121
  innerHtml: entryHtml,
122
+ context: pageContext,
121
123
  }),
122
124
  context: pageContext,
123
125
  });
@@ -147,12 +149,12 @@ export default class ZubyRenderer {
147
149
  jsImports.forEach((imp) => {
148
150
  html = html.replace(/(<\/head>)/, `<script type="module" src="${imp}" defer></script>$1`);
149
151
  });
150
- return new Response(html, {
152
+ return this.injectHeaders(new Response(html, {
151
153
  status: pageContext.statusCode || 200,
152
154
  headers: {
153
155
  'Content-Type': 'text/html;charset=UTF-8',
154
156
  },
155
- });
157
+ }), pageContext);
156
158
  }
157
159
  createPageContext(req) {
158
160
  // Paths starting with /_props are special case.
@@ -161,7 +163,7 @@ export default class ZubyRenderer {
161
163
  const pageUrl = new URL(req.url);
162
164
  if (pageUrl.pathname.startsWith('/_props/')) {
163
165
  pageUrl.pathname = pageUrl.pathname.replace(/^\/_props\//, '/');
164
- req.headers.set('x-zuby-props', 'true');
166
+ req.headers.set(HTTP_HEADERS.XZubyProps, 'true');
165
167
  }
166
168
  return new ZubyPageContext({
167
169
  url: pageUrl,
@@ -169,6 +171,9 @@ export default class ZubyRenderer {
169
171
  statusCode: 200,
170
172
  props: {},
171
173
  zubyContext: this.zubyContext,
174
+ headers: new Headers({
175
+ [HTTP_HEADERS.XPoweredBy]: 'Zuby.js',
176
+ }),
172
177
  });
173
178
  }
174
179
  async render(req) {
@@ -177,4 +182,15 @@ export default class ZubyRenderer {
177
182
  (await this.executeProps(pageContext)) ||
178
183
  (await this.executeEntry(pageContext)));
179
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
+ }
180
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('?') || [];