zuby 1.0.34 → 1.0.35

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/commands/build.js CHANGED
@@ -16,7 +16,7 @@ const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = dirname(__filename);
17
17
  export default async function build(options) {
18
18
  const zubyInternalConfig = await getZubyInternalConfig(options.configFile);
19
- const { vite: viteConfig, customLogger: logger, outDir, srcDir, publicDir, configFilePath, } = zubyInternalConfig;
19
+ const { vite: viteConfig, customLogger: logger, outDir, srcDir, cacheDir, publicDir, configFilePath, } = zubyInternalConfig;
20
20
  process.env.NODE_ENV = MODES.production;
21
21
  logger?.info(getTitle(chalk.gray(`building for production...`)));
22
22
  // Clean build directory
@@ -36,6 +36,7 @@ export default async function build(options) {
36
36
  build: {
37
37
  ...viteConfig?.build,
38
38
  outDir: normalizePath(join(outDir, 'client')),
39
+ ssrManifest: 'chunks-manifest.json',
39
40
  rollupOptions: {
40
41
  ...viteConfig?.build?.rollupOptions,
41
42
  input: {
@@ -43,7 +44,9 @@ export default async function build(options) {
43
44
  },
44
45
  },
45
46
  },
47
+ cacheDir: normalizePath(join(cacheDir, 'client')),
46
48
  mode: MODES.production,
49
+ appType: 'custom',
47
50
  });
48
51
  // Server build
49
52
  logger?.info(`${chalk.bgYellow.bold.whiteBright(` Step 2/4 `)} ${chalk.gray(`building server...`)}`);
@@ -55,7 +58,7 @@ export default async function build(options) {
55
58
  ...viteConfig?.build,
56
59
  outDir: normalizePath(join(outDir, 'server')),
57
60
  ssr: normalizePath(entryFile),
58
- ssrManifest: 'ssr-manifest.json',
61
+ ssrManifest: 'chunks-manifest.json',
59
62
  target: 'node18',
60
63
  },
61
64
  optimizeDeps: {
@@ -65,7 +68,9 @@ export default async function build(options) {
65
68
  force: true,
66
69
  disabled: false,
67
70
  },
71
+ cacheDir: normalizePath(join(cacheDir, 'server')),
68
72
  mode: MODES.production,
73
+ appType: 'custom',
69
74
  });
70
75
  // Add serialized zuby config to build directory
71
76
  writeFileSync(normalizePath(join(outDir, 'zuby.config.json')), JSON.stringify(zubyInternalConfig, null, 2));
package/commands/dev.js CHANGED
@@ -1,39 +1,18 @@
1
- import { MODES } from '../types.js';
2
1
  import { getZubyInternalConfig } from '../config.js';
3
- import { createServer } from 'vite';
4
- import { normalizePath } from '../utils/pathUtils.js';
5
- import { join } from 'path';
6
2
  import { performance } from 'node:perf_hooks';
7
3
  import { getTitle } from '../branding.js';
8
4
  import chalk from 'chalk';
5
+ import ZubyDevServer from '../server/zubyDevServer.js';
9
6
  export default async function dev(options) {
10
- const { vite: viteConfig, customLogger: logger, outDir = '', } = await getZubyInternalConfig(options.configFile);
11
- const port = options.port ? Number(options.port) : viteConfig?.server?.port;
12
- const host = options.host || viteConfig?.server?.host;
13
- const mode = MODES.development;
14
- const serverConfig = {
15
- ...viteConfig,
16
- server: {
17
- ...viteConfig?.server,
18
- port,
19
- host,
20
- },
21
- build: {
22
- outDir: normalizePath(join(outDir, 'client')),
23
- },
24
- mode,
25
- };
7
+ const zubyConfig = await getZubyInternalConfig(options.configFile);
8
+ const { customLogger: logger } = zubyConfig;
26
9
  const startTime = performance.now();
27
- const server = await createServer(serverConfig);
28
- await server.listen();
10
+ const { host = '127.0.0.1', port = 3000 } = zubyConfig.server;
11
+ const zubyDevServer = new ZubyDevServer(zubyConfig);
12
+ await zubyDevServer.listen();
29
13
  const readyTime = performance.now();
30
- if (!server.httpServer) {
31
- throw new Error('HTTP server not available');
32
- }
33
- const bindAddress = server.httpServer.address();
34
- const bindPort = typeof bindAddress === 'string' ? undefined : bindAddress?.port;
35
14
  logger?.info(` ${getTitle(chalk.gray(`started in ${Math.round(readyTime - startTime)}ms`))}\r\n`);
36
- logger?.info(` ┃ Mode ${mode}`);
37
- logger?.info(` ┃ Local http://${host}:${bindPort || port}/`);
15
+ logger?.info(` ┃ Mode development`);
16
+ logger?.info(` ┃ Local http://${host}:${port}/`);
38
17
  logger?.info(` ┃ Network ${chalk.gray('use --host and --port to expose')}\r\n`);
39
18
  }
package/config.js CHANGED
@@ -77,7 +77,8 @@ export const mergeDefaultConfig = async (config) => {
77
77
  // Set default values
78
78
  config.srcDir = config.srcDir ?? './';
79
79
  config.publicDir = config.publicDir ?? 'public';
80
- config.outDir = config.outDir ?? 'build';
80
+ config.outDir = config.outDir ?? '.zuby';
81
+ config.cacheDir = config.cacheDir ?? '.zuby-cache';
81
82
  config.output = config.output ?? 'static';
82
83
  config.prerenderPaths = config.prerenderPaths ?? [];
83
84
  // Default server config
@@ -104,6 +105,7 @@ export const mergeDefaultConfig = async (config) => {
104
105
  config.vite.root = config.vite.root ?? config.srcDir;
105
106
  config.vite.base = config.vite.base ?? '/';
106
107
  config.vite.build.outDir = config.vite.build.outDir ?? config.outDir;
108
+ config.vite.cacheDir = config.vite.cacheDir ?? config.cacheDir;
107
109
  config.vite.logLevel = config.vite.logLevel ?? config.logLevel;
108
110
  config.vite.customLogger = config.vite.customLogger ?? config.customLogger;
109
111
  config.vite.server = config.vite.server ?? config.server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Zuby.js is framework for building SPA apps using Vite",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,7 +1,7 @@
1
1
  import type { PluginOption } from 'vite';
2
2
  import { Template } from '../../templates/types.js';
3
3
  export default function index(): PluginOption;
4
- export declare function generateCompileTimeContextCode(): Promise<string>;
5
- export declare function generateTemplatesCode(): Promise<string>;
4
+ export declare function generateCompileTimeContextCode(ssr: boolean): Promise<string>;
5
+ export declare function generateTemplatesCode(ssr: boolean): Promise<string>;
6
6
  export declare function generateTemplateCode(template: Template): Promise<string>;
7
- export declare function generateRenderCode(): Promise<string>;
7
+ export declare function generateRenderCode(ssr: boolean): Promise<string>;
@@ -11,28 +11,29 @@ export default function index() {
11
11
  async configResolved(config) {
12
12
  viteConfig = config;
13
13
  },
14
- async transform(code, id) {
14
+ async transform(code, id, options) {
15
+ const { ssr = false } = options || {};
15
16
  if (!id.includes('entry') || !/\.(js|ts|jsx|tsx|mjs|cjs)(\?.+)?$/.test(id)) {
16
17
  return;
17
18
  }
18
- const contextCode = await generateCompileTimeContextCode();
19
+ const contextCode = await generateCompileTimeContextCode(ssr);
19
20
  return contextCode + code;
20
21
  },
21
22
  };
22
23
  }
23
- export async function generateCompileTimeContextCode() {
24
+ export async function generateCompileTimeContextCode(ssr) {
24
25
  const { site } = await getZubyInternalConfig();
25
26
  const { version } = await getZubyPackageConfig();
26
27
  return `globalThis.ZubyRawContext = {
27
28
  ...(globalThis.ZubyRawContext || {}),
28
- templates: ${await generateTemplatesCode()},
29
- render: ${await generateRenderCode()},
29
+ templates: ${await generateTemplatesCode(ssr)},
30
+ render: ${await generateRenderCode(ssr)},
30
31
  site: '${site || ''}',
31
32
  generator: 'Zuby.js ${version}',
32
33
  version: '${version}',
33
34
  };`;
34
35
  }
35
- export async function generateTemplatesCode() {
36
+ export async function generateTemplatesCode(ssr) {
36
37
  const templates = await getTemplates();
37
38
  const pages = await getPages(templates);
38
39
  const apps = await getApps(templates);
@@ -43,15 +44,11 @@ export async function generateTemplatesCode() {
43
44
  const pagesCode = await Promise.all(pages.map(generateTemplateCode) || []);
44
45
  const appsCode = await Promise.all(apps.map(generateTemplateCode) || []);
45
46
  const errorsCode = await Promise.all(errors.map(generateTemplateCode) || []);
46
- const layoutsCode = viteConfig?.build.ssr
47
- ? await Promise.all(layouts.map(generateTemplateCode) || [])
48
- : [];
49
- const innerLayoutsCode = viteConfig?.build.ssr
47
+ const layoutsCode = ssr ? await Promise.all(layouts.map(generateTemplateCode) || []) : [];
48
+ const innerLayoutsCode = ssr
50
49
  ? await Promise.all(innerLayouts.map(generateTemplateCode) || [])
51
50
  : [];
52
- const handlersCode = viteConfig?.build.ssr
53
- ? await Promise.all(handlers.map(generateTemplateCode) || [])
54
- : [];
51
+ const handlersCode = ssr ? await Promise.all(handlers.map(generateTemplateCode) || []) : [];
55
52
  return `{
56
53
  pages: [${pagesCode.join(',')}],
57
54
  apps: [${appsCode.join(',')}],
@@ -72,9 +69,9 @@ export async function generateTemplateCode(template) {
72
69
  component: () => import("${template.filename}"),
73
70
  }`;
74
71
  }
75
- export async function generateRenderCode() {
72
+ export async function generateRenderCode(ssr) {
76
73
  const { jsx } = await getZubyInternalConfig();
77
- if (!viteConfig?.build.ssr)
74
+ if (!ssr)
78
75
  return '{}';
79
76
  return `await import("${jsx.renderFile}")`;
80
77
  }
package/server/index.js CHANGED
@@ -751,7 +751,7 @@ var getTitle = (title) => {
751
751
 
752
752
  // src/server/index.ts
753
753
  import { fileURLToPath as fileURLToPath4 } from "url";
754
- import { dirname as dirname2 } from "path";
754
+ import { dirname as dirname2, resolve as resolve4 } from "path";
755
755
 
756
756
  // src/logger/index.ts
757
757
  import readline from "node:readline";
@@ -864,9 +864,7 @@ function createLogger(level = "info", options = {}) {
864
864
 
865
865
  // src/server/zubyServer.ts
866
866
  import { createServer as createHttpsServer } from "node:https";
867
- import {
868
- createServer as createHttpServer
869
- } from "node:http";
867
+ import { createServer as createHttpServer } from "node:http";
870
868
  import { readFileSync as readFileSync3 } from "node:fs";
871
869
 
872
870
  // src/utils/pathUtils.ts
@@ -877,7 +875,7 @@ function normalizePath(path2) {
877
875
 
878
876
  // src/server/zubyServer.ts
879
877
  import { resolve as resolve3 } from "path";
880
- import { createReadStream, existsSync, statSync } from "fs";
878
+ import { createReadStream, existsSync as existsSync2, statSync } from "fs";
881
879
 
882
880
  // src/server/mimeTypes.ts
883
881
  var mimeTypes = Object.freeze({
@@ -2063,9 +2061,6 @@ var mimeTypes = Object.freeze({
2063
2061
  ".zmm": "application/vnd.handheld-entertainment+xml"
2064
2062
  });
2065
2063
 
2066
- // src/server/zubyServer.ts
2067
- import { performance as performance2 } from "node:perf_hooks";
2068
-
2069
2064
  // src/context/index.ts
2070
2065
  var ZubyContext = class {
2071
2066
  constructor(rawContext) {
@@ -5472,10 +5467,10 @@ var Minipass = class extends EventEmitter {
5472
5467
  * Return a void Promise that resolves once the stream ends.
5473
5468
  */
5474
5469
  async promise() {
5475
- return new Promise((resolve4, reject) => {
5470
+ return new Promise((resolve5, reject) => {
5476
5471
  this.on(DESTROYED, () => reject(new Error("stream destroyed")));
5477
5472
  this.on("error", (er) => reject(er));
5478
- this.on("end", () => resolve4());
5473
+ this.on("end", () => resolve5());
5479
5474
  });
5480
5475
  }
5481
5476
  /**
@@ -5499,7 +5494,7 @@ var Minipass = class extends EventEmitter {
5499
5494
  return Promise.resolve({ done: false, value: res });
5500
5495
  if (this[EOF])
5501
5496
  return stop();
5502
- let resolve4;
5497
+ let resolve5;
5503
5498
  let reject;
5504
5499
  const onerr = (er) => {
5505
5500
  this.off("data", ondata);
@@ -5513,19 +5508,19 @@ var Minipass = class extends EventEmitter {
5513
5508
  this.off("end", onend);
5514
5509
  this.off(DESTROYED, ondestroy);
5515
5510
  this.pause();
5516
- resolve4({ value, done: !!this[EOF] });
5511
+ resolve5({ value, done: !!this[EOF] });
5517
5512
  };
5518
5513
  const onend = () => {
5519
5514
  this.off("error", onerr);
5520
5515
  this.off("data", ondata);
5521
5516
  this.off(DESTROYED, ondestroy);
5522
5517
  stop();
5523
- resolve4({ done: true, value: void 0 });
5518
+ resolve5({ done: true, value: void 0 });
5524
5519
  };
5525
5520
  const ondestroy = () => onerr(new Error("stream destroyed"));
5526
5521
  return new Promise((res2, rej) => {
5527
5522
  reject = rej;
5528
- resolve4 = res2;
5523
+ resolve5 = res2;
5529
5524
  this.once(DESTROYED, ondestroy);
5530
5525
  this.once("error", onerr);
5531
5526
  this.once("end", onend);
@@ -6476,9 +6471,9 @@ var PathBase = class {
6476
6471
  if (this.#asyncReaddirInFlight) {
6477
6472
  await this.#asyncReaddirInFlight;
6478
6473
  } else {
6479
- let resolve4 = () => {
6474
+ let resolve5 = () => {
6480
6475
  };
6481
- this.#asyncReaddirInFlight = new Promise((res) => resolve4 = res);
6476
+ this.#asyncReaddirInFlight = new Promise((res) => resolve5 = res);
6482
6477
  try {
6483
6478
  for (const e of await this.#fs.promises.readdir(fullpath, {
6484
6479
  withFileTypes: true
@@ -6491,7 +6486,7 @@ var PathBase = class {
6491
6486
  children.provisional = 0;
6492
6487
  }
6493
6488
  this.#asyncReaddirInFlight = void 0;
6494
- resolve4();
6489
+ resolve5();
6495
6490
  }
6496
6491
  return children.slice(0, children.provisional);
6497
6492
  }
@@ -8352,7 +8347,7 @@ var glob = Object.assign(glob_, {
8352
8347
  glob.glob = glob;
8353
8348
 
8354
8349
  // src/server/zubyRenderer.ts
8355
- import { readFileSync as readFileSync2 } from "fs";
8350
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
8356
8351
 
8357
8352
  // src/templates/types.ts
8358
8353
  var BASE_TEMPLATES = {
@@ -8397,15 +8392,25 @@ var ZubyRenderer = class {
8397
8392
  constructor(outDir) {
8398
8393
  this.outDir = outDir;
8399
8394
  }
8395
+ async reload() {
8396
+ await this.init();
8397
+ }
8400
8398
  async init() {
8401
8399
  const entryPath = normalizePath(resolve2(this.outDir, "server", "entry.js"));
8402
8400
  const entryModule = await import(`file:///${entryPath}`);
8403
8401
  this.entry = entryModule.default || entryModule;
8404
8402
  this.entryClientJs = (await glob(`${this.outDir}/client/chunks/entry-*.js`)).pop();
8405
8403
  this.entryClientCss = (await glob(`${this.outDir}/client/chunks/entry-*.css`)).pop();
8406
- this.ssrManifest = JSON.parse(
8407
- readFileSync2(resolve2(this.outDir, "server", "ssr-manifest.json"), "utf-8")
8408
- );
8404
+ if (this.entryClientJs) {
8405
+ this.entryClientJs = `/chunks/${basename(this.entryClientJs || "")}`;
8406
+ }
8407
+ if (this.entryClientCss) {
8408
+ this.entryClientCss = `/chunks/${basename(this.entryClientCss || "")}`;
8409
+ }
8410
+ const manifestPath = resolve2(this.outDir, "client", "chunks-manifest.json");
8411
+ if (existsSync(manifestPath)) {
8412
+ this.chunksManifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
8413
+ }
8409
8414
  this.zubyContext = getContext();
8410
8415
  this.renderToString = this.zubyContext.renderToString;
8411
8416
  this.renderToStream = this.zubyContext.renderToStream;
@@ -8463,16 +8468,16 @@ var ZubyRenderer = class {
8463
8468
  });
8464
8469
  let html = await this.renderToString?.(layoutChildren) || "";
8465
8470
  const staticImports = [
8466
- ...this.ssrManifest?.[app?.filename || ""] || [],
8467
- ...this.ssrManifest?.[page?.filename || ""] || []
8471
+ ...this.chunksManifest?.[app?.filename || ""] || [],
8472
+ ...this.chunksManifest?.[page?.filename || ""] || []
8468
8473
  ];
8469
8474
  const cssImports = staticImports.filter((imp) => imp.endsWith(".css"));
8470
- const jsImports = [];
8475
+ const jsImports = staticImports.filter((imp) => imp.endsWith(".js"));
8471
8476
  if (this.entryClientCss) {
8472
- cssImports.unshift(`/chunks/${basename(this.entryClientCss)}`);
8477
+ cssImports.unshift(this.entryClientCss);
8473
8478
  }
8474
8479
  if (this.entryClientJs) {
8475
- jsImports.unshift(`/chunks/${basename(this.entryClientJs)}`);
8480
+ jsImports.unshift(this.entryClientJs);
8476
8481
  }
8477
8482
  html = "<!DOCTYPE html>" + html;
8478
8483
  cssImports.forEach((imp) => {
@@ -8514,7 +8519,13 @@ var ZubyServer = class {
8514
8519
  this.options = options;
8515
8520
  this.options.responseHeaders = this.options.responseHeaders || {};
8516
8521
  this.logger = options.logger || createLogger("info");
8517
- this.renderer = new ZubyRenderer(options.outDir);
8522
+ this.renderer = options.renderer || new ZubyRenderer(options.outDir);
8523
+ this.middlewares = [
8524
+ this.defaultHeadersMiddleware.bind(this),
8525
+ this.trailingSlashMiddleware.bind(this),
8526
+ this.clientDirMiddleware.bind(this),
8527
+ this.serverDirMiddleware.bind(this)
8528
+ ];
8518
8529
  const { https, httpsKeyFile, httpsCertFile } = options;
8519
8530
  if (https && httpsKeyFile && httpsCertFile) {
8520
8531
  this.server = createHttpsServer(
@@ -8529,37 +8540,35 @@ var ZubyServer = class {
8529
8540
  }
8530
8541
  }
8531
8542
  async handle(nodeReq, nodeRes) {
8532
- const startTime = performance2.now();
8533
- await this.clientDirHandler(nodeReq, nodeRes) || await this.serverDirHandler(nodeReq, nodeRes);
8534
- const endTime = performance2.now();
8535
- this.logger.info(`Request: ${nodeReq.url} ${nodeRes.statusCode} ${endTime - startTime}ms`);
8543
+ let index = 0;
8544
+ const next = async () => {
8545
+ if (index < this.middlewares.length) {
8546
+ const middleware = this.middlewares[index];
8547
+ index++;
8548
+ await middleware(nodeReq, nodeRes, next);
8549
+ }
8550
+ };
8551
+ await next();
8536
8552
  }
8537
- async clientDirHandler(req, res) {
8553
+ async clientDirMiddleware(req, res, next) {
8538
8554
  const { outDir } = this.options;
8539
8555
  const parsedUrl = new URL(req.url || "", "http://localhost:3000");
8540
8556
  const normalizedPath = normalizePath(parsedUrl.pathname);
8541
8557
  let file = normalizePath(
8542
8558
  resolve3(outDir, "client", normalizedPath.substring(1))
8543
8559
  );
8544
- const exists = existsSync(file);
8560
+ const exists = existsSync2(file);
8545
8561
  const isDirectory = exists && statSync(file).isDirectory();
8546
- if (isDirectory && normalizedPath.length > 0 && !normalizedPath.endsWith("/")) {
8547
- const location = `${parsedUrl.pathname}/${parsedUrl.search || ""}`;
8548
- res.statusCode = 302;
8549
- res.setHeader("Location", location);
8550
- res.end();
8551
- return true;
8552
- }
8553
8562
  if (!exists)
8554
- return false;
8563
+ return next();
8555
8564
  if (isDirectory) {
8556
8565
  file = [
8557
8566
  normalizePath(resolve3(file, "index.html")),
8558
8567
  normalizePath(resolve3(file, "index.json"))
8559
- ].find(existsSync);
8568
+ ].find(existsSync2);
8560
8569
  }
8561
8570
  if (!file)
8562
- return false;
8571
+ return next();
8563
8572
  const fileStats = statSync(file);
8564
8573
  const extension = file.split(".").pop() || "";
8565
8574
  const contentType = mimeTypes[`.${extension}`] || "application/octet-stream";
@@ -8587,47 +8596,52 @@ var ZubyServer = class {
8587
8596
  headers["Content-Length"] = (end - start + 1).toString();
8588
8597
  headers["Accept-Ranges"] = "bytes";
8589
8598
  }
8590
- res.writeHead(statusCode, this.addDefaultHeaders(headers));
8599
+ res.writeHead(statusCode, headers);
8591
8600
  createReadStream(file, streamOptions).pipe(res).on("finish", () => {
8592
8601
  res.end();
8593
8602
  });
8594
- return true;
8595
8603
  }
8596
- async serverDirHandler(nodeReq, nodeRes) {
8604
+ async serverDirMiddleware(nodeReq, nodeRes) {
8597
8605
  const req = await this.toRequest(nodeReq);
8598
8606
  const res = await this.renderer.render(req);
8599
8607
  return this.toNodeResponse(res, nodeRes);
8600
8608
  }
8601
- async start() {
8602
- const serverPromise = new Promise((resolve4, reject) => {
8609
+ async listen() {
8610
+ const serverPromise = new Promise((resolve5, reject) => {
8603
8611
  this.server.listen(this.options.port, this.options.host, 511, () => {
8604
8612
  if (!this.server.listening)
8605
8613
  return reject(new Error(`Server could not start`));
8606
- resolve4(void 0);
8614
+ resolve5(void 0);
8607
8615
  });
8608
8616
  });
8609
8617
  const rendererPromise = this.renderer.init();
8610
8618
  await Promise.all([serverPromise, rendererPromise]);
8611
8619
  }
8612
- async stop() {
8620
+ async close() {
8613
8621
  this.server.closeAllConnections();
8614
- return new Promise((resolve4, reject) => {
8622
+ return new Promise((resolve5, reject) => {
8615
8623
  this.server.close((error) => {
8616
8624
  if (error)
8617
8625
  return reject(error);
8618
- resolve4(void 0);
8626
+ resolve5(void 0);
8619
8627
  });
8620
8628
  });
8621
8629
  }
8622
- addDefaultHeaders(headers) {
8623
- return {
8624
- "X-Powered-By": "Zuby.js",
8625
- ...this.options.responseHeaders,
8626
- ...headers
8627
- };
8630
+ defaultHeadersMiddleware(_req, res, next) {
8631
+ res.setHeader("X-Powered-By", "Zuby.js");
8632
+ next();
8633
+ }
8634
+ trailingSlashMiddleware(req, res, next) {
8635
+ const [path2, query] = req.url?.split("?") || [];
8636
+ if (path2.endsWith("/") || path2.match(/[.](.+)$/)) {
8637
+ return next();
8638
+ }
8639
+ res.statusCode = 302;
8640
+ res.setHeader("Location", `${path2}/${query ? `?${query}` : ""}`);
8641
+ res.end();
8628
8642
  }
8629
8643
  async toRequest(nodeReq) {
8630
- return new Promise((resolve4, reject) => {
8644
+ return new Promise((resolve5, reject) => {
8631
8645
  try {
8632
8646
  let buffer = [];
8633
8647
  nodeReq.on("data", (chunk) => buffer.push(chunk)).on("end", () => {
@@ -8639,7 +8653,7 @@ var ZubyServer = class {
8639
8653
  for (const key in nodeReq.headers) {
8640
8654
  headers.append(key, nodeReq.headers[key]);
8641
8655
  }
8642
- resolve4(
8656
+ resolve5(
8643
8657
  new Request(url, {
8644
8658
  method: nodeReq.method,
8645
8659
  body: nodeReq.method === "POST" ? body : null,
@@ -8655,27 +8669,38 @@ var ZubyServer = class {
8655
8669
  async toNodeResponse(res, nodeRes) {
8656
8670
  const headers = {};
8657
8671
  res.headers.forEach((value, key) => headers[key] = value);
8658
- nodeRes.writeHead(res.status, this.addDefaultHeaders(headers));
8672
+ nodeRes.writeHead(res.status, headers);
8659
8673
  nodeRes.end(await res.text());
8660
8674
  }
8675
+ use(middleware) {
8676
+ this.middlewares.push(middleware);
8677
+ }
8678
+ useBefore(middleware) {
8679
+ this.middlewares.unshift(middleware);
8680
+ }
8681
+ async reload() {
8682
+ await this.renderer.reload();
8683
+ }
8661
8684
  };
8662
8685
 
8663
8686
  // src/server/index.ts
8687
+ import { readFileSync as readFileSync4 } from "fs";
8664
8688
  var __filename = fileURLToPath4(import.meta.url);
8665
8689
  var __dirname = dirname2(__filename);
8666
8690
  var logger = createLogger("info");
8691
+ var config = JSON.parse(readFileSync4(resolve4(__dirname, "zuby.config.json"), "utf-8"));
8667
8692
  var mode = process.env["NODE_ENV"] || "production";
8668
8693
  var envPort = process.env["PORT"] ? Number(process.env["PORT"]) : void 0;
8669
8694
  var envHost = process.env["HOST"];
8670
- var port = envPort || 3e3;
8671
- var host = envHost || "127.0.0.1";
8695
+ var port = envPort || config?.server?.port || 3e3;
8696
+ var host = envHost || config?.server?.host || "127.0.0.1";
8672
8697
  var zubyServer = new ZubyServer({
8673
8698
  port,
8674
8699
  host,
8675
8700
  outDir: __dirname
8676
8701
  });
8677
8702
  if (mode === "production") {
8678
- await zubyServer.start();
8703
+ await zubyServer.listen();
8679
8704
  logger?.info(` ${getTitle("")}\r
8680
8705
  `);
8681
8706
  logger?.info(` \u2503 Mode ${"production"}`);
package/server/types.d.ts CHANGED
@@ -1,4 +1,9 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  import { ZubyLogger } from '../logger/types.js';
3
+ import { IncomingMessage, ServerResponse } from 'node:http';
4
+ import ZubyRenderer from './zubyRenderer.js';
5
+ export type NodeRequest = IncomingMessage;
6
+ export type NodeResponse = ServerResponse;
2
7
  export type ZubyHeaders = Record<string, string | string[] | undefined>;
3
8
  export interface ZubyServerOptions {
4
9
  port: number;
@@ -9,4 +14,6 @@ export interface ZubyServerOptions {
9
14
  httpsKeyFile?: string;
10
15
  logger?: ZubyLogger;
11
16
  responseHeaders?: ZubyHeaders;
17
+ renderer?: ZubyRenderer;
12
18
  }
19
+ export type ZubyMiddleware = (req: NodeRequest, res: NodeResponse, next: () => void | Promise<void>) => void | Promise<void>;
@@ -0,0 +1,10 @@
1
+ import ZubyRenderer from './zubyRenderer.js';
2
+ import { ZubyInternalConfig } from '../types.js';
3
+ import { ViteDevServer } from 'vite';
4
+ export default class ZubyDevRenderer extends ZubyRenderer {
5
+ protected zubyInternalConfig: ZubyInternalConfig;
6
+ protected viteServerDevServer: ViteDevServer;
7
+ constructor(zubyConfig: ZubyInternalConfig, viteServerDevServer: ViteDevServer);
8
+ init(): Promise<void>;
9
+ render(req: Request): Promise<Response>;
10
+ }
@@ -0,0 +1,37 @@
1
+ import { normalizePath } from '../utils/pathUtils.js';
2
+ import { relative } from 'path';
3
+ import { getContext } from '../context/index.js';
4
+ import ZubyRenderer from './zubyRenderer.js';
5
+ import { getEntryFile } from '../commands/build.js';
6
+ export default class ZubyDevRenderer extends ZubyRenderer {
7
+ constructor(zubyConfig, viteServerDevServer) {
8
+ super(zubyConfig.outDir);
9
+ this.zubyInternalConfig = zubyConfig;
10
+ this.viteServerDevServer = viteServerDevServer;
11
+ }
12
+ async init() {
13
+ const entryPath = this.zubyInternalConfig.jsx.entryTemplateFile;
14
+ const entryModule = await this.viteServerDevServer.ssrLoadModule(entryPath);
15
+ this.entry = entryModule.default || entryModule;
16
+ this.zubyContext = getContext();
17
+ this.renderToString = this.zubyContext.renderToString;
18
+ this.renderToStream = this.zubyContext.renderToStream;
19
+ // Load the entry file from the project directory
20
+ // or jsxProvider
21
+ const entryFile = await getEntryFile(this.zubyInternalConfig);
22
+ this.entryClientJs = '/' + normalizePath(relative(process.cwd(), entryFile));
23
+ }
24
+ async render(req) {
25
+ const originalRes = await super.render(req);
26
+ // Do not transform non-html responses
27
+ if (!originalRes.headers.get('content-type')?.includes('text/html'))
28
+ return originalRes;
29
+ const body = await originalRes.text();
30
+ const bodyTransformed = await this.viteServerDevServer.transformIndexHtml(req.url, body);
31
+ return new Response(bodyTransformed, {
32
+ headers: originalRes.headers,
33
+ status: originalRes.status,
34
+ statusText: originalRes.statusText,
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ import ZubyServer from './zubyServer.js';
2
+ import { ZubyInternalConfig } from '../types.js';
3
+ import { ViteDevServer } from 'vite';
4
+ export default class ZubyDevServer extends ZubyServer {
5
+ protected zubyInternalConfig: ZubyInternalConfig;
6
+ protected viteClientDevServer?: ViteDevServer;
7
+ protected viteServerDevServer?: ViteDevServer;
8
+ constructor(zubyConfig: ZubyInternalConfig);
9
+ listen(): Promise<void>;
10
+ reload(): Promise<void>;
11
+ }
@@ -0,0 +1,117 @@
1
+ import ZubyServer from './zubyServer.js';
2
+ import { MODES } from '../types.js';
3
+ import ZubyDevRenderer from './zubyDevRenderer.js';
4
+ import { createServer } from 'vite';
5
+ import { normalizePath } from '../utils/pathUtils.js';
6
+ import { join } from 'path';
7
+ import { getEntryFile } from '../commands/build.js';
8
+ import { rmSync, watch } from 'fs';
9
+ export default class ZubyDevServer extends ZubyServer {
10
+ constructor(zubyConfig) {
11
+ super({
12
+ port: zubyConfig.server.port || 3000,
13
+ host: zubyConfig.server.host || '127.0.0.1',
14
+ outDir: zubyConfig.outDir,
15
+ });
16
+ this.zubyInternalConfig = zubyConfig;
17
+ this.middlewares = [
18
+ this.defaultHeadersMiddleware.bind(this),
19
+ this.trailingSlashMiddleware.bind(this),
20
+ this.serverDirMiddleware.bind(this),
21
+ ];
22
+ }
23
+ async listen() {
24
+ // Clear cache directory
25
+ rmSync(this.zubyInternalConfig.cacheDir, {
26
+ recursive: true,
27
+ force: true,
28
+ });
29
+ // Load the entry file from the project directory
30
+ // or jsxProvider
31
+ const entryFile = await getEntryFile(this.zubyInternalConfig);
32
+ this.viteClientDevServer = await createServer({
33
+ configFile: false,
34
+ ...this.zubyInternalConfig.vite,
35
+ server: {
36
+ ...this.zubyInternalConfig.vite?.server,
37
+ middlewareMode: true,
38
+ port: this.options.port,
39
+ host: this.options.host,
40
+ hmr: {
41
+ port: this.options.port + 1,
42
+ host: this.options.host,
43
+ protocol: 'ws',
44
+ },
45
+ },
46
+ build: {
47
+ ...this.zubyInternalConfig.vite?.build,
48
+ outDir: normalizePath(join(this.zubyInternalConfig.outDir, 'client')),
49
+ ssr: false,
50
+ ssrManifest: 'chunks-manifest.json',
51
+ rollupOptions: {
52
+ ...this.zubyInternalConfig.vite?.build?.rollupOptions,
53
+ input: {
54
+ entry: normalizePath(entryFile),
55
+ },
56
+ },
57
+ watch: {
58
+ include: `${this.zubyInternalConfig.srcDir}/**/*`,
59
+ },
60
+ },
61
+ cacheDir: normalizePath(join(this.zubyInternalConfig.cacheDir, 'client')),
62
+ mode: MODES.development,
63
+ appType: 'custom',
64
+ });
65
+ this.viteServerDevServer = await createServer({
66
+ ...this.zubyInternalConfig.vite,
67
+ server: {
68
+ ...this.zubyInternalConfig.vite?.server,
69
+ middlewareMode: true,
70
+ port: this.options.port,
71
+ host: this.options.host,
72
+ hmr: {
73
+ port: this.options.port + 2,
74
+ host: this.options.host,
75
+ protocol: 'ws',
76
+ },
77
+ },
78
+ configFile: false,
79
+ publicDir: false,
80
+ build: {
81
+ ...this.zubyInternalConfig.vite?.build,
82
+ outDir: normalizePath(join(this.zubyInternalConfig.outDir, 'server')),
83
+ ssr: normalizePath(entryFile),
84
+ ssrManifest: 'chunks-manifest.json',
85
+ ssrEmitAssets: true,
86
+ target: 'node18',
87
+ watch: {
88
+ include: `${this.zubyInternalConfig.srcDir}/**/*`,
89
+ },
90
+ },
91
+ optimizeDeps: {
92
+ ...this.zubyInternalConfig.vite.optimizeDeps,
93
+ entries: ['**/*'],
94
+ include: ['**/*'],
95
+ force: true,
96
+ disabled: false,
97
+ },
98
+ cacheDir: normalizePath(join(this.zubyInternalConfig.cacheDir, 'server')),
99
+ mode: MODES.development,
100
+ appType: 'custom',
101
+ });
102
+ this.useBefore(this.viteClientDevServer.middlewares);
103
+ this.useBefore(this.viteServerDevServer.middlewares);
104
+ this.renderer = new ZubyDevRenderer(this.zubyInternalConfig, this.viteServerDevServer);
105
+ watch(join(this.zubyInternalConfig.srcDir, 'pages'), { recursive: true }, (eventType, fileName) => {
106
+ if (eventType === 'rename')
107
+ this.reload.bind(this)();
108
+ });
109
+ await this.renderer.init();
110
+ await super.listen();
111
+ }
112
+ async reload() {
113
+ this.viteClientDevServer?.moduleGraph.invalidateAll();
114
+ this.viteServerDevServer?.moduleGraph.invalidateAll();
115
+ await this.renderer.reload();
116
+ }
117
+ }
@@ -10,10 +10,11 @@ export default class ZubyRenderer {
10
10
  protected entry?: EntryType;
11
11
  protected entryClientJs?: string;
12
12
  protected entryClientCss?: string;
13
- protected ssrManifest?: {
13
+ protected chunksManifest?: {
14
14
  [key: string]: string[];
15
15
  };
16
16
  constructor(outDir: string);
17
+ reload(): Promise<void>;
17
18
  init(): Promise<void>;
18
19
  executeHandler(pageContext: ZubyPageContext): Promise<undefined | Response>;
19
20
  executeProps(pageContext: ZubyPageContext): Promise<undefined | Response>;
@@ -3,19 +3,31 @@ import { ZubyPageContext } from '../pageContext/index.js';
3
3
  import { normalizePath } from '../utils/pathUtils.js';
4
4
  import { basename, resolve } from 'path';
5
5
  import { glob } from 'glob';
6
- import { readFileSync } from 'fs';
6
+ import { existsSync, readFileSync } from 'fs';
7
7
  import { findMatchingTemplate } from '../templates/pathUtils.js';
8
8
  export default class ZubyRenderer {
9
9
  constructor(outDir) {
10
10
  this.outDir = outDir;
11
11
  }
12
+ async reload() {
13
+ await this.init();
14
+ }
12
15
  async init() {
13
16
  const entryPath = normalizePath(resolve(this.outDir, 'server', 'entry.js'));
14
17
  const entryModule = await import(`file:///${entryPath}`);
15
18
  this.entry = entryModule.default || entryModule;
16
19
  this.entryClientJs = (await glob(`${this.outDir}/client/chunks/entry-*.js`)).pop();
17
20
  this.entryClientCss = (await glob(`${this.outDir}/client/chunks/entry-*.css`)).pop();
18
- this.ssrManifest = JSON.parse(readFileSync(resolve(this.outDir, 'server', 'ssr-manifest.json'), 'utf-8'));
21
+ if (this.entryClientJs) {
22
+ this.entryClientJs = `/chunks/${basename(this.entryClientJs || '')}`;
23
+ }
24
+ if (this.entryClientCss) {
25
+ this.entryClientCss = `/chunks/${basename(this.entryClientCss || '')}`;
26
+ }
27
+ const manifestPath = resolve(this.outDir, 'client', 'chunks-manifest.json');
28
+ if (existsSync(manifestPath)) {
29
+ this.chunksManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
30
+ }
19
31
  this.zubyContext = getContext();
20
32
  this.renderToString = this.zubyContext.renderToString;
21
33
  this.renderToStream = this.zubyContext.renderToStream;
@@ -82,18 +94,18 @@ export default class ZubyRenderer {
82
94
  // Render the layout
83
95
  let html = (await this.renderToString?.(layoutChildren)) || '';
84
96
  const staticImports = [
85
- ...(this.ssrManifest?.[app?.filename || ''] || []),
86
- ...(this.ssrManifest?.[page?.filename || ''] || []),
97
+ ...(this.chunksManifest?.[app?.filename || ''] || []),
98
+ ...(this.chunksManifest?.[page?.filename || ''] || []),
87
99
  ];
88
100
  const cssImports = staticImports.filter((imp) => imp.endsWith('.css'));
89
- const jsImports = [];
101
+ const jsImports = staticImports.filter((imp) => imp.endsWith('.js'));
90
102
  // Entry css
91
103
  if (this.entryClientCss) {
92
- cssImports.unshift(`/chunks/${basename(this.entryClientCss)}`);
104
+ cssImports.unshift(this.entryClientCss);
93
105
  }
94
106
  // Entry js
95
107
  if (this.entryClientJs) {
96
- jsImports.unshift(`/chunks/${basename(this.entryClientJs)}`);
108
+ jsImports.unshift(this.entryClientJs);
97
109
  }
98
110
  // Insert document type
99
111
  html = '<!DOCTYPE html>' + html;
@@ -1,26 +1,27 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  /// <reference types="node" resolution-mode="require"/>
3
3
  import { Server as HttpsServer } from 'node:https';
4
- import { Server as HttpServer, IncomingMessage, ServerResponse } from 'node:http';
5
- import { ZubyHeaders, ZubyServerOptions } from './types.js';
4
+ import { Server as HttpServer, ServerResponse } from 'node:http';
5
+ import { ZubyServerOptions, NodeResponse, NodeRequest, ZubyMiddleware } from './types.js';
6
6
  import { ZubyLogger } from '../logger/types.js';
7
7
  import ZubyRenderer from './zubyRenderer.js';
8
- export type NodeRequest = IncomingMessage;
9
- export type NodeResponse = ServerResponse;
10
8
  export default class ZubyServer {
11
9
  protected options: ZubyServerOptions;
12
10
  protected server: HttpServer | HttpsServer;
13
11
  protected logger: ZubyLogger;
14
12
  protected renderer: ZubyRenderer;
13
+ protected middlewares: ZubyMiddleware[];
15
14
  constructor(options: ZubyServerOptions);
16
15
  handle(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
17
- clientDirHandler(req: NodeRequest, res: NodeResponse): Promise<boolean | NodeResponse>;
18
- serverDirHandler(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
19
- start(): Promise<void>;
20
- stop(): Promise<void>;
21
- addDefaultHeaders(headers: ZubyHeaders): {
22
- 'X-Powered-By': string;
23
- };
16
+ clientDirMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): Promise<void | NodeResponse>;
17
+ serverDirMiddleware(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
18
+ listen(): Promise<void>;
19
+ close(): Promise<void>;
20
+ defaultHeadersMiddleware(_req: NodeRequest, res: NodeResponse, next: () => void): void;
21
+ trailingSlashMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): void;
24
22
  toRequest(nodeReq: NodeRequest): Promise<Request>;
25
23
  toNodeResponse(res: Response, nodeRes: ServerResponse): Promise<void>;
24
+ use(middleware: ZubyMiddleware): void;
25
+ useBefore(middleware: ZubyMiddleware): void;
26
+ reload(): Promise<void>;
26
27
  }
@@ -1,19 +1,24 @@
1
1
  import { createServer as createHttpsServer } from 'node:https';
2
- import { createServer as createHttpServer, } from 'node:http';
2
+ import { createServer as createHttpServer } from 'node:http';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { normalizePath } from '../utils/pathUtils.js';
5
5
  import { resolve } from 'path';
6
6
  import { createReadStream, existsSync, statSync } from 'fs';
7
7
  import { mimeTypes } from './mimeTypes.js';
8
8
  import { createLogger } from '../logger/index.js';
9
- import { performance } from 'node:perf_hooks';
10
9
  import ZubyRenderer from './zubyRenderer.js';
11
10
  export default class ZubyServer {
12
11
  constructor(options) {
13
12
  this.options = options;
14
13
  this.options.responseHeaders = this.options.responseHeaders || {};
15
14
  this.logger = options.logger || createLogger('info');
16
- this.renderer = new ZubyRenderer(options.outDir);
15
+ this.renderer = options.renderer || new ZubyRenderer(options.outDir);
16
+ this.middlewares = [
17
+ this.defaultHeadersMiddleware.bind(this),
18
+ this.trailingSlashMiddleware.bind(this),
19
+ this.clientDirMiddleware.bind(this),
20
+ this.serverDirMiddleware.bind(this),
21
+ ];
17
22
  const { https, httpsKeyFile, httpsCertFile } = options;
18
23
  if (https && httpsKeyFile && httpsCertFile) {
19
24
  // Create the HTTPS server
@@ -28,41 +33,27 @@ export default class ZubyServer {
28
33
  }
29
34
  }
30
35
  async handle(nodeReq, nodeRes) {
31
- const startTime = performance.now();
32
- (await this.clientDirHandler(nodeReq, nodeRes)) ||
33
- (await this.serverDirHandler(nodeReq, nodeRes));
34
- const endTime = performance.now();
35
- this.logger.info(`Request: ${nodeReq.url} ${nodeRes.statusCode} ${endTime - startTime}ms`);
36
- // try {
37
- // const startTime = performance.now();
38
- // await this.clientDirHandler(nodeReq, nodeRes) || await this.serverDirHandler(nodeReq, nodeRes);
39
- // const endTime = performance.now();
40
- // this.logger.info(`Request: ${nodeReq.url} ${nodeRes.statusCode} ${endTime - startTime}ms`);
41
- // } catch (error: any) {
42
- // this.logger.error(error?.message);
43
- // nodeRes.statusCode = 500;
44
- // nodeRes.end('Internal server error');
45
- // }
36
+ let index = 0;
37
+ const next = async () => {
38
+ if (index < this.middlewares.length) {
39
+ const middleware = this.middlewares[index];
40
+ index++;
41
+ await middleware(nodeReq, nodeRes, next);
42
+ }
43
+ };
44
+ await next();
46
45
  }
47
- async clientDirHandler(req, res) {
46
+ async clientDirMiddleware(req, res, next) {
48
47
  const { outDir } = this.options;
49
48
  const parsedUrl = new URL(req.url || '', 'http://localhost:3000');
50
49
  const normalizedPath = normalizePath(parsedUrl.pathname);
51
50
  let file = normalizePath(resolve(outDir, 'client', normalizedPath.substring(1)));
52
51
  const exists = existsSync(file);
53
52
  const isDirectory = exists && statSync(file).isDirectory();
54
- if (isDirectory && normalizedPath.length > 0 && !normalizedPath.endsWith('/')) {
55
- // Redirect to path with trailing slash for consistency
56
- const location = `${parsedUrl.pathname}/${parsedUrl.search || ''}`;
57
- res.statusCode = 302;
58
- res.setHeader('Location', location);
59
- res.end();
60
- return true;
61
- }
62
53
  // Do nothing if the file does not exist.
63
54
  // ssrHandler will handle the request.
64
55
  if (!exists)
65
- return false;
56
+ return next();
66
57
  // If the requested file is a directory, try to load the index file
67
58
  if (isDirectory) {
68
59
  file = [
@@ -73,7 +64,7 @@ export default class ZubyServer {
73
64
  // Server will handle the request
74
65
  // if we could not find the file
75
66
  if (!file)
76
- return false;
67
+ return next();
77
68
  const fileStats = statSync(file);
78
69
  const extension = file.split('.').pop() || '';
79
70
  const contentType = mimeTypes[`.${extension}`] || 'application/octet-stream';
@@ -103,22 +94,21 @@ export default class ZubyServer {
103
94
  headers['Content-Length'] = (end - start + 1).toString();
104
95
  headers['Accept-Ranges'] = 'bytes';
105
96
  }
106
- // Set headers
107
- res.writeHead(statusCode, this.addDefaultHeaders(headers));
97
+ // Write the headers
98
+ res.writeHead(statusCode, headers);
108
99
  // Stream the file
109
100
  createReadStream(file, streamOptions)
110
101
  .pipe(res)
111
102
  .on('finish', () => {
112
103
  res.end();
113
104
  });
114
- return true;
115
105
  }
116
- async serverDirHandler(nodeReq, nodeRes) {
106
+ async serverDirMiddleware(nodeReq, nodeRes) {
117
107
  const req = await this.toRequest(nodeReq);
118
108
  const res = await this.renderer.render(req);
119
109
  return this.toNodeResponse(res, nodeRes);
120
110
  }
121
- async start() {
111
+ async listen() {
122
112
  const serverPromise = new Promise((resolve, reject) => {
123
113
  this.server.listen(this.options.port, this.options.host, 511, () => {
124
114
  if (!this.server.listening)
@@ -129,7 +119,7 @@ export default class ZubyServer {
129
119
  const rendererPromise = this.renderer.init();
130
120
  await Promise.all([serverPromise, rendererPromise]);
131
121
  }
132
- async stop() {
122
+ async close() {
133
123
  this.server.closeAllConnections();
134
124
  return new Promise((resolve, reject) => {
135
125
  this.server.close(error => {
@@ -139,12 +129,21 @@ export default class ZubyServer {
139
129
  });
140
130
  });
141
131
  }
142
- addDefaultHeaders(headers) {
143
- return {
144
- 'X-Powered-By': 'Zuby.js',
145
- ...this.options.responseHeaders,
146
- ...headers,
147
- };
132
+ defaultHeadersMiddleware(_req, res, next) {
133
+ res.setHeader('X-Powered-By', 'Zuby.js');
134
+ next();
135
+ }
136
+ trailingSlashMiddleware(req, res, next) {
137
+ // Redirect to path with trailing slash for consistency
138
+ const [path, query] = req.url?.split('?') || [];
139
+ // Do nothing if the path already has a trailing slash
140
+ // or if the path has file extension
141
+ if (path.endsWith('/') || path.match(/[.](.+)$/)) {
142
+ return next();
143
+ }
144
+ res.statusCode = 302;
145
+ res.setHeader('Location', `${path}/${query ? `?${query}` : ''}`);
146
+ res.end();
148
147
  }
149
148
  async toRequest(nodeReq) {
150
149
  return new Promise((resolve, reject) => {
@@ -176,7 +175,16 @@ export default class ZubyServer {
176
175
  async toNodeResponse(res, nodeRes) {
177
176
  const headers = {};
178
177
  res.headers.forEach((value, key) => (headers[key] = value));
179
- nodeRes.writeHead(res.status, this.addDefaultHeaders(headers));
178
+ nodeRes.writeHead(res.status, headers);
180
179
  nodeRes.end(await res.text());
181
180
  }
181
+ use(middleware) {
182
+ this.middlewares.push(middleware);
183
+ }
184
+ useBefore(middleware) {
185
+ this.middlewares.unshift(middleware);
186
+ }
187
+ async reload() {
188
+ await this.renderer.reload();
189
+ }
182
190
  }