zuby 1.0.64 → 1.0.65

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
@@ -5,6 +5,8 @@ import { randomBytes } from 'crypto';
5
5
  import { bundleRequire } from 'bundle-require';
6
6
  import { createLogger } from './logger/index.js';
7
7
  import { TEMPLATES } from './templates/types.js';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
8
10
  // Plugins
9
11
  import contextPlugin from './plugins/contextPlugin/index.js';
10
12
  import chunkNamingPlugin from './plugins/chunkNamingPlugin/index.js';
@@ -13,6 +15,9 @@ import prerenderPlugin from './plugins/prerenderPlugin/index.js';
13
15
  import standaloneBuildPlugin from './plugins/dependenciesPlugin/index.js';
14
16
  import preloadPlugin from './plugins/preloadPlugin/index.js';
15
17
  import injectPlugin from './plugins/injectPlugin/index.js';
18
+ import { normalizePath } from './utils/pathUtils.js';
19
+ import { dirname, resolve } from 'path';
20
+ import { fileURLToPath } from 'url';
16
21
  let zubyInternalConfig;
17
22
  /**
18
23
  * Returns the path to the ZubyConfig file.
@@ -163,6 +168,13 @@ export const mergeDefaultConfig = async (config) => {
163
168
  config.minifyJS = config.minifyJS ?? true;
164
169
  config.compress = config.compress ?? true;
165
170
  config.poweredByHeader = config.poweredByHeader ?? true;
171
+ // Image config
172
+ config.image = config.image ?? {};
173
+ config.image.sizes = config.image.sizes ?? [160, 320, 640, 768, 1024, 1280, 1536, 1920, 2048];
174
+ config.image.loader =
175
+ config.image.loader ?? normalizePath(resolve(__dirname, 'image', 'imageLoader.js'));
176
+ config.image.defaultFormat = config.image.defaultFormat ?? 'webp';
177
+ config.image.defaultQuality = config.image.defaultQuality ?? 80;
166
178
  // Build ID generator
167
179
  config.generateBuildId = config.generateBuildId ?? generateDefaultBuildId;
168
180
  // Global props
@@ -1,5 +1,5 @@
1
1
  import { LazyTemplate } from '../templates/types.js';
2
- import { RenderToStream, RenderToString } from '../types.js';
2
+ import { ImageFormat, ImageLoader, RenderToStream, RenderToString } from '../types.js';
3
3
  export interface GlobalContext {
4
4
  /**
5
5
  * The array with templates.
@@ -56,6 +56,37 @@ export interface GlobalContext {
56
56
  * on both client and server side.
57
57
  */
58
58
  props: Record<string, any>;
59
+ /**
60
+ * The image component options.
61
+ */
62
+ image?: {
63
+ /**
64
+ * The path to the image loader function.
65
+ * @default 'zuby/imageLoader.js'
66
+ */
67
+ loader?: ImageLoader;
68
+ /**
69
+ * The array of image sizes
70
+ * that will be used to generate image src.
71
+ * Zuby.js will try to use the closest size to the actual specified size.
72
+ */
73
+ sizes?: number[];
74
+ /**
75
+ * The default format of the image,
76
+ * that will be passed to the image loader function
77
+ * if the format is not specified.
78
+ * @default 'webp'
79
+ */
80
+ defaultFormat?: ImageFormat;
81
+ /**
82
+ * The default quality of the image,
83
+ * that will be passed to the image loader function
84
+ * if the quality is not specified.
85
+ * This should be a number between 0 and 100.
86
+ * @default 75
87
+ */
88
+ defaultQuality?: number;
89
+ };
59
90
  /**
60
91
  * The global server props for the site
61
92
  * that will be passed to all pages
@@ -0,0 +1,18 @@
1
+ import { GlobalContext } from '../contexts/index.js';
2
+ /**
3
+ * Adjust the image size
4
+ * to closest matching size from the image.sizes config.
5
+ * @param context - GlobalContext
6
+ * @param height - The height of the image
7
+ * @param width - The width of the image
8
+ */
9
+ export default function getNearestSize(context: GlobalContext, width: number | undefined, height: number | undefined): {
10
+ width: number | undefined;
11
+ height: number | undefined;
12
+ };
13
+ /**
14
+ * Returns the nearest size from the array of sizes.
15
+ * @param size - The size to compare
16
+ * @param sizes - The array of sizes
17
+ */
18
+ export declare function getNearestSizeFromArray(size: number, sizes: number[]): number;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Adjust the image size
3
+ * to closest matching size from the image.sizes config.
4
+ * @param context - GlobalContext
5
+ * @param height - The height of the image
6
+ * @param width - The width of the image
7
+ */
8
+ export default function getNearestSize(context, width, height) {
9
+ const sizes = context.image?.sizes || [];
10
+ if (width && height) {
11
+ const heightRatio = height / width;
12
+ width = getNearestSizeFromArray(width, sizes);
13
+ height = width * heightRatio;
14
+ }
15
+ else if (width) {
16
+ width = getNearestSizeFromArray(width, sizes);
17
+ }
18
+ else if (height) {
19
+ height = getNearestSizeFromArray(height, sizes);
20
+ }
21
+ return { width, height };
22
+ }
23
+ /**
24
+ * Returns the nearest size from the array of sizes.
25
+ * @param size - The size to compare
26
+ * @param sizes - The array of sizes
27
+ */
28
+ export function getNearestSizeFromArray(size, sizes) {
29
+ return sizes.reduce((prev, curr) => {
30
+ return Math.abs(curr - size) < Math.abs(prev - size) ? curr : prev;
31
+ });
32
+ }
@@ -0,0 +1,8 @@
1
+ import { ImageLoaderOptions } from '../types.js';
2
+ /**
3
+ * The default image loader for Zuby.js
4
+ * that is used to generate image URLs
5
+ * for your project when <Image> component is used.
6
+ * @param options - ImageLoaderOptions
7
+ */
8
+ export default function imageLoader({ context, src }: ImageLoaderOptions): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The default image loader for Zuby.js
3
+ * that is used to generate image URLs
4
+ * for your project when <Image> component is used.
5
+ * @param options - ImageLoaderOptions
6
+ */
7
+ export default function imageLoader({ context, src }) {
8
+ const { buildId } = context;
9
+ // Add buildId to the image URL
10
+ if (src.includes('?'))
11
+ return `${src}&${buildId}`;
12
+ return `${src}?${buildId}`;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.64",
3
+ "version": "1.0.65",
4
4
  "description": "Zuby.js is a framework for building modern apps using Preact or React.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -6,4 +6,6 @@ export declare function generateTemplatesCode(ssr: boolean): Promise<string>;
6
6
  export declare function generateTemplateCode(template: Template): Promise<string>;
7
7
  export declare function generateImportCode(template: Template): string;
8
8
  export declare function generateRenderCode(ssr: boolean): Promise<string>;
9
+ export declare function generateImageCode(): Promise<string>;
9
10
  export declare function getStaticImportsCode(): string;
11
+ export declare function getStaticImportkey(): string;
@@ -36,6 +36,7 @@ export async function generateCompileTimeContextCode(ssr) {
36
36
  version: '${version}',
37
37
  buildId: '${buildId}',
38
38
  props: ${JSON.stringify(props)},
39
+ image: ${await generateImageCode()},
39
40
  serverProps: ${JSON.stringify(ssr ? serverProps : {})},
40
41
  headElements: ${JSON.stringify(ssr ? headElements : [])},
41
42
  bodyElements: ${JSON.stringify(ssr ? bodyElements : [])},
@@ -84,7 +85,7 @@ export async function generateTemplateCode(template) {
84
85
  export function generateImportCode(template) {
85
86
  // Sync templates are imported statically
86
87
  if (Object.values(SYNC_TEMPLATES).includes(template.templateType)) {
87
- const key = `__zuby_static_import_${staticImports.length}`;
88
+ const key = getStaticImportkey();
88
89
  staticImports.push({
89
90
  key,
90
91
  path: template.filename,
@@ -100,6 +101,26 @@ export async function generateRenderCode(ssr) {
100
101
  return '{}';
101
102
  return `await import("${jsx.renderFile}")`;
102
103
  }
104
+ export async function generateImageCode() {
105
+ const { image } = await getZubyInternalConfig();
106
+ let loaderImport = undefined;
107
+ if (image?.loader) {
108
+ loaderImport = getStaticImportkey();
109
+ staticImports.push({
110
+ key: loaderImport,
111
+ path: image.loader,
112
+ });
113
+ }
114
+ return `{
115
+ ${image?.sizes ? `sizes: ${JSON.stringify(image.sizes)},` : ''}
116
+ ${image?.loader ? `loader: ${loaderImport},` : ''}
117
+ ${image?.defaultFormat ? `defaultFormat: "${image.defaultFormat}",` : ''}
118
+ ${image?.defaultQuality ? `defaultQuality: "${image.defaultQuality}",` : ''}
119
+ }`;
120
+ }
103
121
  export function getStaticImportsCode() {
104
122
  return staticImports.map(({ key, path }) => `import ${key} from "${path}";`).join('\n');
105
123
  }
124
+ export function getStaticImportkey() {
125
+ return `__zuby_static_import_${staticImports.length}`;
126
+ }
@@ -5,11 +5,12 @@ import { performance } from 'node:perf_hooks';
5
5
  import ZubyRenderer from '../../server/zubyRenderer.js';
6
6
  import { writeFile } from 'fs/promises';
7
7
  import { getPages } from '../../templates/index.js';
8
- import { OUTPUTS } from '../../types.js';
8
+ import { OUTPUTS, PLUGIN_HOOKS } from '../../types.js';
9
9
  import { PATH_TYPES } from '../../templates/types.js';
10
10
  import { substitutePathParams } from '../../templates/pathUtils.js';
11
11
  import { normalizePath } from '../../utils/pathUtils.js';
12
12
  import { HTTP_HEADERS, ZUBY_USER_AGENTS } from '../../constants.js';
13
+ import { executePlugins } from '../../config.js';
13
14
  /**
14
15
  * This is internal plugin
15
16
  * that pre-renders the pages during the build.
@@ -83,6 +84,10 @@ export default function prerenderPlugin() {
83
84
  // Assign all discovered prerender paths to the config for other plugins.
84
85
  config.prerenderPaths = prerenderPaths;
85
86
  for (const path of prerenderPaths) {
87
+ // Run pre-render start hook
88
+ await executePlugins(config, PLUGIN_HOOKS.ZubyPrerenderStart, {
89
+ path,
90
+ });
86
91
  const pageReq = new Request(new URL(path, reqBaseUrl), reqOptions);
87
92
  const propsReq = new Request(new URL(`/_props${path}`, reqBaseUrl), reqOptions);
88
93
  const [pageRes, propsRes] = await Promise.all([
@@ -104,6 +109,11 @@ export default function prerenderPlugin() {
104
109
  });
105
110
  });
106
111
  await Promise.all([writeFile(pageTarget, page), writeFile(propsTarget, props)]);
112
+ // Run pre-render done hook
113
+ await executePlugins(config, PLUGIN_HOOKS.ZubyPrerenderDone, {
114
+ path,
115
+ body: page,
116
+ });
107
117
  logger.info('[prerender] Pre-rendered path: ' + path);
108
118
  }
109
119
  const finishTime = performance.now();
package/server/index.js CHANGED
@@ -2563,7 +2563,7 @@ var ZubyServer = class {
2563
2563
  headers[HTTP_HEADERS.ContentLength] = (end - start + 1).toString();
2564
2564
  headers[HTTP_HEADERS.AcceptRanges] = "bytes";
2565
2565
  }
2566
- const encoding = this.getEncoding(req);
2566
+ const encoding = this.shouldCompress(headers) ? this.getEncoding(req) : ENCODINGS.none;
2567
2567
  const compressionStream = this.getCompressionStream(encoding);
2568
2568
  if (encoding !== ENCODINGS.none) {
2569
2569
  headers[HTTP_HEADERS.ContentEncoding] = encoding;
@@ -2576,9 +2576,9 @@ var ZubyServer = class {
2576
2576
  }
2577
2577
  async serverDirMiddleware(nodeReq, nodeRes, _next) {
2578
2578
  const req = await this.toRequest(nodeReq);
2579
- const encoding = this.getEncoding(nodeReq);
2580
2579
  const cacheKey = new URL(req.url, "http://localhost").pathname;
2581
2580
  const res = this.cacheRead(cacheKey) || this.cacheWrite(cacheKey, await this.renderer.render(req));
2581
+ const encoding = this.getEncoding(nodeReq);
2582
2582
  return this.toNodeResponse(res, nodeRes, encoding);
2583
2583
  }
2584
2584
  cacheRead(key) {
@@ -2666,6 +2666,7 @@ var ZubyServer = class {
2666
2666
  async toNodeResponse(res, nodeRes, encoding = ENCODINGS.none) {
2667
2667
  const headers = {};
2668
2668
  res.headers.forEach((value, key) => headers[key] = value);
2669
+ encoding = this.shouldCompress(headers) ? encoding : ENCODINGS.none;
2669
2670
  const bodyBuffer = Buffer.from(await res.arrayBuffer());
2670
2671
  const bodyStream = Readable.from(bodyBuffer);
2671
2672
  const compressionStream = this.getCompressionStream(encoding);
@@ -2702,6 +2703,10 @@ var ZubyServer = class {
2702
2703
  };
2703
2704
  return compressionMap[encoding];
2704
2705
  }
2706
+ shouldCompress(resHeaders) {
2707
+ const contentType = (resHeaders[HTTP_HEADERS.ContentType] || resHeaders[HTTP_HEADERS.ContentType.toLowerCase()] || "").toString();
2708
+ return contentType.startsWith("text/") || contentType.startsWith("application/json");
2709
+ }
2705
2710
  };
2706
2711
 
2707
2712
  // src/server/index.ts
@@ -3,7 +3,7 @@
3
3
  /// <reference types="node" resolution-mode="require"/>
4
4
  import { Server as HttpsServer } from 'node:https';
5
5
  import { Server as HttpServer, ServerResponse } from 'node:http';
6
- import { ZubyServerOptions, NodeResponse, NodeRequest, ZubyMiddleware, Encoding } from './types.js';
6
+ import { ZubyHeaders, ZubyServerOptions, NodeResponse, NodeRequest, ZubyMiddleware, Encoding } from './types.js';
7
7
  import { ZubyLogger } from '../logger/types.js';
8
8
  import ZubyRenderer from './zubyRenderer.js';
9
9
  import { PassThrough } from 'stream';
@@ -30,4 +30,5 @@ export default class ZubyServer {
30
30
  reload(): Promise<void>;
31
31
  getEncoding(nodeReq: NodeRequest): string;
32
32
  getCompressionStream(encoding: Encoding): PassThrough;
33
+ shouldCompress(resHeaders: ZubyHeaders): boolean;
33
34
  }
@@ -106,7 +106,8 @@ export default class ZubyServer {
106
106
  headers[HTTP_HEADERS.ContentLength] = (end - start + 1).toString();
107
107
  headers[HTTP_HEADERS.AcceptRanges] = 'bytes';
108
108
  }
109
- const encoding = this.getEncoding(req);
109
+ // We will compress only txt/json responses for performance reasons
110
+ const encoding = this.shouldCompress(headers) ? this.getEncoding(req) : ENCODINGS.none;
110
111
  const compressionStream = this.getCompressionStream(encoding);
111
112
  if (encoding !== ENCODINGS.none) {
112
113
  headers[HTTP_HEADERS.ContentEncoding] = encoding;
@@ -124,9 +125,9 @@ export default class ZubyServer {
124
125
  }
125
126
  async serverDirMiddleware(nodeReq, nodeRes, _next) {
126
127
  const req = await this.toRequest(nodeReq);
127
- const encoding = this.getEncoding(nodeReq);
128
128
  const cacheKey = new URL(req.url, 'http://localhost').pathname;
129
129
  const res = this.cacheRead(cacheKey) || this.cacheWrite(cacheKey, await this.renderer.render(req));
130
+ const encoding = this.getEncoding(nodeReq);
130
131
  return this.toNodeResponse(res, nodeRes, encoding);
131
132
  }
132
133
  cacheRead(key) {
@@ -220,6 +221,8 @@ export default class ZubyServer {
220
221
  async toNodeResponse(res, nodeRes, encoding = ENCODINGS.none) {
221
222
  const headers = {};
222
223
  res.headers.forEach((value, key) => (headers[key] = value));
224
+ // We will compress only txt/json responses for performance reasons
225
+ encoding = this.shouldCompress(headers) ? encoding : ENCODINGS.none;
223
226
  const bodyBuffer = Buffer.from(await res.arrayBuffer());
224
227
  const bodyStream = Readable.from(bodyBuffer);
225
228
  const compressionStream = this.getCompressionStream(encoding);
@@ -265,4 +268,10 @@ export default class ZubyServer {
265
268
  };
266
269
  return compressionMap[encoding];
267
270
  }
271
+ shouldCompress(resHeaders) {
272
+ const contentType = (resHeaders[HTTP_HEADERS.ContentType] ||
273
+ resHeaders[HTTP_HEADERS.ContentType.toLowerCase()] ||
274
+ '').toString();
275
+ return contentType.startsWith('text/') || contentType.startsWith('application/json');
276
+ }
268
277
  }
package/types.d.ts CHANGED
@@ -4,6 +4,7 @@ import { ZubyLogger } from './logger/types.js';
4
4
  import ReadableStream = NodeJS.ReadableStream;
5
5
  import { PathParamsType, Template, TemplateFile } from './templates/types.js';
6
6
  import ZubyDevServer from './server/zubyDevServer.js';
7
+ import { GlobalContext } from './contexts/index.js';
7
8
  export interface ZubyConfig {
8
9
  /**
9
10
  * The JSX provider which will be used to render the pages.
@@ -192,6 +193,37 @@ export interface ZubyConfig {
192
193
  * @private
193
194
  */
194
195
  entryScriptsBottom?: string[];
196
+ /**
197
+ * The image component options.
198
+ */
199
+ image?: {
200
+ /**
201
+ * The path to the image loader function.
202
+ * @default 'zuby/imageLoader.js'
203
+ */
204
+ loader?: string;
205
+ /**
206
+ * The array of image sizes
207
+ * that will be used to generate image src.
208
+ * Zuby.js will try to use the closest size to the actual specified size.
209
+ */
210
+ sizes?: number[];
211
+ /**
212
+ * The default format of the image,
213
+ * that will be passed to the image loader function
214
+ * if the format is not specified.
215
+ * @default 'webp'
216
+ */
217
+ defaultFormat?: ImageFormat;
218
+ /**
219
+ * The default quality of the image,
220
+ * that will be passed to the image loader function
221
+ * if the quality is not specified.
222
+ * This should be a number between 0 and 100.
223
+ * @default 75
224
+ */
225
+ defaultQuality?: number;
226
+ };
195
227
  }
196
228
  export interface ZubyInternalConfig extends Required<ZubyConfig> {
197
229
  /**
@@ -377,6 +409,16 @@ export interface ZubyPlugin extends VitePlugin {
377
409
  * This hook is called after the server build is done
378
410
  */
379
411
  'zuby:build:server:done'?: (params: ZubyBuildHookParams) => void | Promise<void>;
412
+ /**
413
+ * This hook is called for each path
414
+ * before the pre-rendering process starts
415
+ */
416
+ 'zuby:prerender:start'?: (params: ZubyPrerenderStartParams) => void | Promise<void>;
417
+ /**
418
+ * This hook is called for each path
419
+ * after the pre-rendering process ends
420
+ */
421
+ 'zuby:prerender:done'?: (params: ZubyPrerenderDoneParams) => void | Promise<void>;
380
422
  /**
381
423
  * This hook is called after the whole build process is done
382
424
  */
@@ -410,6 +452,14 @@ export interface ZubyPreviewHookParams {
410
452
  config: ZubyInternalConfig;
411
453
  logger: ZubyLogger;
412
454
  }
455
+ export interface ZubyPrerenderStartParams {
456
+ config: ZubyInternalConfig;
457
+ logger: ZubyLogger;
458
+ path: string;
459
+ }
460
+ export interface ZubyPrerenderDoneParams extends ZubyPrerenderStartParams {
461
+ body: string;
462
+ }
413
463
  export interface ZubyBuildHookParams {
414
464
  config: ZubyInternalConfig;
415
465
  logger: ZubyLogger;
@@ -430,6 +480,8 @@ export declare const PLUGIN_HOOKS: {
430
480
  ZubyDevSetup: string;
431
481
  ZubyDevStart: string;
432
482
  ZubyPreviewStart: string;
483
+ ZubyPrerenderStart: string;
484
+ ZubyPrerenderDone: string;
433
485
  ZubyBuildSetup: string;
434
486
  ZubyBuildStart: string;
435
487
  ZubyBuildClientDone: string;
@@ -437,3 +489,53 @@ export declare const PLUGIN_HOOKS: {
437
489
  ZubyBuildDone: string;
438
490
  };
439
491
  export type PluginHook = (typeof PLUGIN_HOOKS)[keyof typeof PLUGIN_HOOKS];
492
+ /**
493
+ * The options for the ImageLoader.
494
+ */
495
+ export interface ImageLoaderOptions {
496
+ /**
497
+ * The GlobalContext
498
+ */
499
+ context: GlobalContext;
500
+ /**
501
+ * The path to the image file.
502
+ */
503
+ src: string;
504
+ /**
505
+ * True if the provided src is an absolute path.
506
+ */
507
+ isAbsolute?: boolean;
508
+ /**
509
+ * The width of the image.
510
+ */
511
+ width?: number;
512
+ /**
513
+ * The height of the image.
514
+ */
515
+ height?: number;
516
+ /**
517
+ * The quality of the image.
518
+ */
519
+ quality?: number;
520
+ /**
521
+ * The format of the image.
522
+ */
523
+ format?: ImageFormat;
524
+ }
525
+ export declare const IMAGE_FORMATS: {
526
+ readonly WEBP: "webp";
527
+ readonly JPG: "jpg";
528
+ readonly JPEG: "jpeg";
529
+ readonly PNG: "png";
530
+ readonly AVIF: "avif";
531
+ readonly GIF: "gif";
532
+ };
533
+ export declare const IMAGE_FORMATS_ARRAY: string[];
534
+ export type ImageFormat = (typeof IMAGE_FORMATS)[keyof typeof IMAGE_FORMATS];
535
+ /**
536
+ * The ImageLoader function that is used to
537
+ * generate src attribute for the image component
538
+ * based on the given options.
539
+ * @param options The options for the image loader.
540
+ */
541
+ export type ImageLoader = (options: ImageLoaderOptions) => string;
package/types.js CHANGED
@@ -16,6 +16,10 @@ export const PLUGIN_HOOKS = {
16
16
  ZubyDevStart: 'zuby:dev:start',
17
17
  // Preview cmd
18
18
  ZubyPreviewStart: 'zuby:preview:start',
19
+ // Pre-rendering hooks
20
+ // called for each path
21
+ ZubyPrerenderStart: 'zuby:prerender:start',
22
+ ZubyPrerenderDone: 'zuby:prerender:done',
19
23
  // Build cmd
20
24
  ZubyBuildSetup: 'zuby:build:setup',
21
25
  ZubyBuildStart: 'zuby:build:start',
@@ -23,3 +27,12 @@ export const PLUGIN_HOOKS = {
23
27
  ZubyBuildServerDone: 'zuby:build:server:done',
24
28
  ZubyBuildDone: 'zuby:build:done',
25
29
  };
30
+ export const IMAGE_FORMATS = {
31
+ WEBP: 'webp',
32
+ JPG: 'jpg',
33
+ JPEG: 'jpeg',
34
+ PNG: 'png',
35
+ AVIF: 'avif',
36
+ GIF: 'gif',
37
+ };
38
+ export const IMAGE_FORMATS_ARRAY = Object.values(IMAGE_FORMATS);