@esmx/core 3.0.0-rc.77 → 3.0.0-rc.79

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/dist/core.d.ts CHANGED
@@ -3,8 +3,7 @@ import { type App } from './app';
3
3
  import { type ManifestJson } from './manifest-json';
4
4
  import { type ModuleConfig, type ParsedModuleConfig } from './module-config';
5
5
  import { type PackConfig, type ParsedPackConfig } from './pack-config';
6
- import type { ImportmapMode } from './render-context';
7
- import type { RenderContext, RenderContextOptions } from './render-context';
6
+ import type { ImportmapMode, RenderContext, RenderContextOptions } from './render-context';
8
7
  import type { Middleware } from './utils/middleware';
9
8
  import { type ProjectPath } from './utils/resolve-path';
10
9
  /**
@@ -696,4 +695,20 @@ export declare class Esmx {
696
695
  * ```
697
696
  */
698
697
  getStaticImportPaths(env: BuildEnvironment, specifier: string): Promise<readonly string[] | null>;
698
+ /**
699
+ * Generate bundle size analysis report for the build artifacts.
700
+ *
701
+ * @returns {{ text: string, json: object }} Report with text and JSON formats
702
+ *
703
+ * @example
704
+ * ```ts
705
+ * const report = esmx.generateSizeReport();
706
+ * console.log(report.text);
707
+ * console.log(`Total files: ${report.json.totalFiles}`);
708
+ * ```
709
+ */
710
+ generateSizeReport(): {
711
+ text: string;
712
+ json: import("./utils/file-size-stats").SizeStatsReport;
713
+ };
699
714
  }
package/dist/core.mjs CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  parsePackConfig
15
15
  } from "./pack-config.mjs";
16
16
  import { createCache } from "./utils/cache.mjs";
17
+ import { generateSizeReport } from "./utils/file-size-stats.mjs";
17
18
  import { createClientImportMap, createImportMap } from "./utils/import-map.mjs";
18
19
  import { resolvePath } from "./utils/resolve-path.mjs";
19
20
  import { getImportPreloadInfo as getStaticImportPaths } from "./utils/static-import-lexer.mjs";
@@ -872,6 +873,24 @@ document.head.appendChild(script);
872
873
  }
873
874
  );
874
875
  }
876
+ /**
877
+ * Generate bundle size analysis report for the build artifacts.
878
+ *
879
+ * @returns {{ text: string, json: object }} Report with text and JSON formats
880
+ *
881
+ * @example
882
+ * ```ts
883
+ * const report = esmx.generateSizeReport();
884
+ * console.log(report.text);
885
+ * console.log(`Total files: ${report.json.totalFiles}`);
886
+ * ```
887
+ */
888
+ generateSizeReport() {
889
+ return generateSizeReport(
890
+ this.resolvePath("dist"),
891
+ "{client,server,node}/**/!(.*)"
892
+ );
893
+ }
875
894
  }
876
895
  async function defaultDevApp() {
877
896
  throw new Error("'devApp' function not set");
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { type EsmxOptions, type COMMAND, type BuildEnvironment, type ImportMap, type SpecifierMap, type ScopesMap, Esmx } from './core';
2
- export type { ModuleConfig, ModuleConfigImportMapping, ModuleConfigExportExports, ModuleConfigExportExport, ModuleConfigExportObject, ParsedModuleConfig, ParsedModuleConfigExports, ParsedModuleConfigExport, ParsedModuleConfigEnvironment, ParsedModuleConfigLink } from './module-config';
3
- export type { PackConfig, ParsedPackConfig } from './pack-config';
4
1
  export { type App, createApp } from './app';
5
- export { type RenderContextOptions, type ServerRenderHandle, type RenderFiles, RenderContext } from './render-context';
6
- export { isImmutableFile, type Middleware, createMiddleware, mergeMiddlewares } from './utils/middleware';
2
+ export { type BuildEnvironment, type COMMAND, Esmx, type EsmxOptions, type ImportMap, type ScopesMap, type SpecifierMap } from './core';
7
3
  export type { ManifestJson, ManifestJsonChunk, ManifestJsonChunks, ManifestJsonExport, ManifestJsonExports } from './manifest-json';
4
+ export type { ModuleConfig, ModuleConfigExportExport, ModuleConfigExportExports, ModuleConfigExportObject, ModuleConfigImportMapping, ParsedModuleConfig, ParsedModuleConfigEnvironment, ParsedModuleConfigExport, ParsedModuleConfigExports, ParsedModuleConfigLink } from './module-config';
5
+ export type { PackConfig, ParsedPackConfig } from './pack-config';
6
+ export { RenderContext, type RenderContextOptions, type RenderFiles, type ServerRenderHandle } from './render-context';
7
+ export { createMiddleware, isImmutableFile, type Middleware, mergeMiddlewares } from './utils/middleware';
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
+ export { createApp } from "./app.mjs";
1
2
  export {
2
3
  Esmx
3
4
  } from "./core.mjs";
4
- export { createApp } from "./app.mjs";
5
5
  export {
6
6
  RenderContext
7
7
  } from "./render-context.mjs";
8
8
  export {
9
- isImmutableFile,
10
9
  createMiddleware,
10
+ isImmutableFile,
11
11
  mergeMiddlewares
12
12
  } from "./utils/middleware.mjs";
@@ -8,8 +8,8 @@ import {
8
8
  getEnvironmentScopes,
9
9
  getEnvironments,
10
10
  getLinks,
11
- parseModuleConfig,
12
11
  parsedExportValue,
12
+ parseModuleConfig,
13
13
  processExportArray,
14
14
  processObjectExport,
15
15
  processStringExport,
@@ -0,0 +1,29 @@
1
+ export interface FileInfo {
2
+ path: string;
3
+ relativePath: string;
4
+ size: number;
5
+ gzipSize: number;
6
+ ext: string;
7
+ }
8
+ export interface SizeStatsReport {
9
+ totalFiles: number;
10
+ totalSize: number;
11
+ totalGzipSize: number;
12
+ compressionRatio: number;
13
+ files: FileInfo[];
14
+ byExtension: Record<string, {
15
+ count: number;
16
+ totalSize: number;
17
+ totalGzipSize: number;
18
+ avgSize: number;
19
+ avgGzipSize: number;
20
+ }>;
21
+ }
22
+ export declare function analyzeDirectory(dirPath: string, pattern?: string): SizeStatsReport;
23
+ export declare function formatSize(bytes: number): string;
24
+ export declare function generateTextReport(report: SizeStatsReport): string;
25
+ export declare function generateJsonReport(report: SizeStatsReport): string;
26
+ export declare function generateSizeReport(dirPath: string, pattern?: string): {
27
+ text: string;
28
+ json: SizeStatsReport;
29
+ };
@@ -0,0 +1,120 @@
1
+ import fs, { globSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { gzipSync } from "node:zlib";
4
+ function getGzipSize(filePath) {
5
+ try {
6
+ const content = fs.readFileSync(filePath);
7
+ const compressed = gzipSync(content, { level: 9 });
8
+ return compressed.length;
9
+ } catch (error) {
10
+ return fs.statSync(filePath).size;
11
+ }
12
+ }
13
+ function getAllFiles(dirPath, pattern = "**/!(.*)") {
14
+ const files = globSync(pattern, {
15
+ cwd: dirPath
16
+ });
17
+ return files.map((relativePath) => path.resolve(dirPath, relativePath)).filter((filePath) => fs.statSync(filePath).isFile());
18
+ }
19
+ export function analyzeDirectory(dirPath, pattern) {
20
+ if (!fs.existsSync(dirPath)) {
21
+ throw new Error(`Directory does not exist: ${dirPath}`);
22
+ }
23
+ const stat = fs.statSync(dirPath);
24
+ if (!stat.isDirectory()) {
25
+ throw new Error(`Path is not a directory: ${dirPath}`);
26
+ }
27
+ const files = getAllFiles(dirPath, pattern);
28
+ const fileInfos = [];
29
+ const byExtension = {};
30
+ let totalSize = 0;
31
+ let totalGzipSize = 0;
32
+ for (const filePath of files) {
33
+ const fileStat = fs.statSync(filePath);
34
+ const size = fileStat.size;
35
+ const gzipSize = getGzipSize(filePath);
36
+ const relativePath = path.relative(process.cwd(), filePath);
37
+ const ext = path.extname(filePath).toLowerCase() || "(no ext)";
38
+ const fileInfo = {
39
+ path: filePath,
40
+ relativePath,
41
+ size,
42
+ gzipSize,
43
+ ext
44
+ };
45
+ fileInfos.push(fileInfo);
46
+ totalSize += size;
47
+ totalGzipSize += gzipSize;
48
+ if (!byExtension[ext]) {
49
+ byExtension[ext] = {
50
+ count: 0,
51
+ totalSize: 0,
52
+ totalGzipSize: 0,
53
+ avgSize: 0,
54
+ avgGzipSize: 0
55
+ };
56
+ }
57
+ byExtension[ext].count++;
58
+ byExtension[ext].totalSize += size;
59
+ byExtension[ext].totalGzipSize += gzipSize;
60
+ }
61
+ Object.values(byExtension).forEach((group) => {
62
+ group.avgSize = Math.round(group.totalSize / group.count);
63
+ group.avgGzipSize = Math.round(group.totalGzipSize / group.count);
64
+ });
65
+ fileInfos.sort((a, b) => b.size - a.size);
66
+ const compressionRatio = totalSize > 0 ? (totalSize - totalGzipSize) / totalSize * 100 : 0;
67
+ return {
68
+ totalFiles: files.length,
69
+ totalSize,
70
+ totalGzipSize,
71
+ compressionRatio: Math.round(compressionRatio * 100) / 100,
72
+ files: fileInfos,
73
+ byExtension
74
+ };
75
+ }
76
+ export function formatSize(bytes) {
77
+ const units = ["B", "KB", "MB", "GB"];
78
+ let size = bytes;
79
+ let unitIndex = 0;
80
+ while (size >= 1024 && unitIndex < units.length - 1) {
81
+ size /= 1024;
82
+ unitIndex++;
83
+ }
84
+ return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
85
+ }
86
+ export function generateTextReport(report) {
87
+ const lines = [];
88
+ lines.push("\u{1F4CA} Bundle Analysis");
89
+ lines.push("=".repeat(50));
90
+ lines.push("");
91
+ const maxPathLength = Math.max(
92
+ ...report.files.map((f) => f.relativePath.length)
93
+ );
94
+ const sizeHeader = "Size".padStart(10);
95
+ const gzippedHeader = "Gzipped".padStart(10);
96
+ const header = `File${" ".repeat(maxPathLength - 4)} ${sizeHeader} ${gzippedHeader}`;
97
+ lines.push(header);
98
+ lines.push("-".repeat(header.length));
99
+ for (const file of report.files) {
100
+ const paddedPath = file.relativePath.padEnd(maxPathLength);
101
+ const sizeStr = formatSize(file.size).padStart(10);
102
+ const gzipStr = formatSize(file.gzipSize).padStart(10);
103
+ lines.push(`${paddedPath} ${sizeStr} ${gzipStr}`);
104
+ }
105
+ lines.push("");
106
+ lines.push(
107
+ `Total: ${report.totalFiles} files, ${formatSize(report.totalSize)} (gzipped: ${formatSize(report.totalGzipSize)})`
108
+ );
109
+ return lines.join("\n");
110
+ }
111
+ export function generateJsonReport(report) {
112
+ return JSON.stringify(report, null, 2);
113
+ }
114
+ export function generateSizeReport(dirPath, pattern) {
115
+ const json = analyzeDirectory(dirPath, pattern);
116
+ return {
117
+ text: generateTextReport(json),
118
+ json
119
+ };
120
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,183 @@
1
+ import fs from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { assert, describe, test } from "vitest";
5
+ import {
6
+ analyzeDirectory,
7
+ formatSize,
8
+ generateSizeReport,
9
+ generateTextReport
10
+ } from "./file-size-stats.mjs";
11
+ describe("formatSize", () => {
12
+ test("should format bytes correctly", () => {
13
+ assert.equal(formatSize(0), "0 B");
14
+ assert.equal(formatSize(512), "512 B");
15
+ assert.equal(formatSize(1024), "1.0 KB");
16
+ assert.equal(formatSize(1536), "1.5 KB");
17
+ assert.equal(formatSize(1048576), "1.0 MB");
18
+ assert.equal(formatSize(1073741824), "1.0 GB");
19
+ });
20
+ });
21
+ describe("analyzeDirectory", () => {
22
+ test("should throw error for non-existent directory", () => {
23
+ try {
24
+ analyzeDirectory("/non-existent-path");
25
+ assert.fail("Should have thrown an error");
26
+ } catch (error) {
27
+ assert.equal(
28
+ error.message,
29
+ "Directory does not exist: /non-existent-path"
30
+ );
31
+ }
32
+ });
33
+ test("should throw error for file path", () => {
34
+ const tempFile = path.join(tmpdir(), "test-file.txt");
35
+ fs.writeFileSync(tempFile, "test content");
36
+ try {
37
+ try {
38
+ analyzeDirectory(tempFile);
39
+ assert.fail("Should have thrown an error");
40
+ } catch (error) {
41
+ assert.equal(
42
+ error.message,
43
+ `Path is not a directory: ${tempFile}`
44
+ );
45
+ }
46
+ } finally {
47
+ fs.unlinkSync(tempFile);
48
+ }
49
+ });
50
+ test("should analyze empty directory", () => {
51
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), "test-"));
52
+ try {
53
+ const report = analyzeDirectory(tempDir);
54
+ assert.equal(report.totalFiles, 0);
55
+ assert.equal(report.totalSize, 0);
56
+ assert.equal(report.totalGzipSize, 0);
57
+ assert.equal(report.compressionRatio, 0);
58
+ assert.deepEqual(report.files, []);
59
+ assert.deepEqual(report.byExtension, {});
60
+ } finally {
61
+ fs.rmSync(tempDir, { recursive: true, force: true });
62
+ }
63
+ });
64
+ test("should analyze directory with files", () => {
65
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), "test-"));
66
+ try {
67
+ const jsContent = 'console.log("hello world");'.repeat(100);
68
+ const cssContent = "body { color: red; }".repeat(50);
69
+ const jsonContent = JSON.stringify({ test: "data" });
70
+ fs.writeFileSync(path.join(tempDir, "test.js"), jsContent);
71
+ fs.writeFileSync(path.join(tempDir, "style.css"), cssContent);
72
+ fs.writeFileSync(path.join(tempDir, "data.json"), jsonContent);
73
+ const subDir = path.join(tempDir, "subdir");
74
+ fs.mkdirSync(subDir);
75
+ fs.writeFileSync(path.join(subDir, "nested.js"), jsContent);
76
+ const report = analyzeDirectory(tempDir);
77
+ assert.equal(report.totalFiles, 4);
78
+ assert.ok(report.totalSize > 0);
79
+ assert.ok(report.totalGzipSize > 0);
80
+ assert.ok(report.compressionRatio >= 0);
81
+ assert.ok(report.byExtension[".js"]);
82
+ assert.ok(report.byExtension[".css"]);
83
+ assert.ok(report.byExtension[".json"]);
84
+ assert.equal(report.byExtension[".js"].count, 2);
85
+ assert.equal(report.byExtension[".css"].count, 1);
86
+ assert.equal(report.byExtension[".json"].count, 1);
87
+ assert.equal(report.files.length, 4);
88
+ const sizes = report.files.map((f) => f.size);
89
+ assert.deepEqual(
90
+ sizes,
91
+ [...sizes].sort((a, b) => b - a)
92
+ );
93
+ } finally {
94
+ fs.rmSync(tempDir, { recursive: true, force: true });
95
+ }
96
+ });
97
+ });
98
+ describe("generateTextReport", () => {
99
+ test("should generate text report", () => {
100
+ const report = {
101
+ totalFiles: 2,
102
+ totalSize: 2048,
103
+ totalGzipSize: 1024,
104
+ compressionRatio: 50,
105
+ files: [
106
+ {
107
+ path: "/test/large.js",
108
+ relativePath: "large.js",
109
+ size: 1536,
110
+ gzipSize: 768,
111
+ ext: ".js"
112
+ },
113
+ {
114
+ path: "/test/small.css",
115
+ relativePath: "small.css",
116
+ size: 512,
117
+ gzipSize: 256,
118
+ ext: ".css"
119
+ }
120
+ ],
121
+ byExtension: {
122
+ ".js": {
123
+ count: 1,
124
+ totalSize: 1536,
125
+ totalGzipSize: 768,
126
+ avgSize: 1536,
127
+ avgGzipSize: 768
128
+ },
129
+ ".css": {
130
+ count: 1,
131
+ totalSize: 512,
132
+ totalGzipSize: 256,
133
+ avgSize: 512,
134
+ avgGzipSize: 256
135
+ }
136
+ }
137
+ };
138
+ const text = generateTextReport(report);
139
+ assert.ok(text.includes("\u{1F4CA} Bundle Analysis"));
140
+ assert.ok(text.includes("large.js"));
141
+ assert.ok(text.includes("small.css"));
142
+ assert.ok(text.includes("Total: 2 files"));
143
+ });
144
+ });
145
+ describe("generateSizeReport", () => {
146
+ test("should generate report with text and json properties", () => {
147
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), "test-"));
148
+ try {
149
+ fs.writeFileSync(
150
+ path.join(tempDir, "test.js"),
151
+ 'console.log("test");'
152
+ );
153
+ const report = generateSizeReport(tempDir);
154
+ assert.ok(typeof report === "object");
155
+ assert.ok(typeof report.text === "string");
156
+ assert.ok(typeof report.json === "object");
157
+ assert.ok(report.text.includes("\u{1F4CA} Bundle Analysis"));
158
+ assert.ok(!report.text.startsWith("{"));
159
+ assert.ok(report.json.totalFiles !== void 0);
160
+ } finally {
161
+ fs.rmSync(tempDir, { recursive: true, force: true });
162
+ }
163
+ });
164
+ test("should generate report with pattern filter", () => {
165
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), "test-"));
166
+ try {
167
+ fs.writeFileSync(
168
+ path.join(tempDir, "test.js"),
169
+ 'console.log("test");'
170
+ );
171
+ fs.writeFileSync(
172
+ path.join(tempDir, "test.css"),
173
+ "body { color: red; }"
174
+ );
175
+ const report = generateSizeReport(tempDir, "*.js");
176
+ assert.ok(typeof report === "object");
177
+ assert.ok(report.json.totalFiles === 1);
178
+ assert.ok(report.json.files[0].ext === ".js");
179
+ } finally {
180
+ fs.rmSync(tempDir, { recursive: true, force: true });
181
+ }
182
+ });
183
+ });
package/package.json CHANGED
@@ -59,7 +59,7 @@
59
59
  "build": "unbuild"
60
60
  },
61
61
  "dependencies": {
62
- "@esmx/import": "3.0.0-rc.77",
62
+ "@esmx/import": "3.0.0-rc.79",
63
63
  "@types/serialize-javascript": "^5.0.4",
64
64
  "es-module-lexer": "^1.7.0",
65
65
  "find": "^0.3.0",
@@ -67,9 +67,9 @@
67
67
  "serialize-javascript": "^6.0.2"
68
68
  },
69
69
  "devDependencies": {
70
- "@biomejs/biome": "1.9.4",
70
+ "@biomejs/biome": "2.3.7",
71
71
  "@types/find": "^0.2.4",
72
- "@types/node": "^24.10.0",
72
+ "@types/node": "^24.0.0",
73
73
  "@types/send": "^1.2.1",
74
74
  "@types/write": "^2.0.4",
75
75
  "@vitest/coverage-v8": "3.2.4",
@@ -77,7 +77,7 @@
77
77
  "unbuild": "3.6.1",
78
78
  "vitest": "3.2.4"
79
79
  },
80
- "version": "3.0.0-rc.77",
80
+ "version": "3.0.0-rc.79",
81
81
  "type": "module",
82
82
  "private": false,
83
83
  "exports": {
@@ -100,5 +100,5 @@
100
100
  "template",
101
101
  "public"
102
102
  ],
103
- "gitHead": "c56120965914f33eca4b21336f705b3cb5fc7f93"
103
+ "gitHead": "29c95da5062140daffe10ba135ba66bae6e65fc6"
104
104
  }
package/src/app.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  type RenderContextOptions,
7
7
  type ServerRenderHandle
8
8
  } from './render-context';
9
- import { type Middleware, createMiddleware } from './utils/middleware';
9
+ import { createMiddleware, type Middleware } from './utils/middleware';
10
10
 
11
11
  /**
12
12
  * Application instance interface.
package/src/cli/index.ts CHANGED
@@ -6,6 +6,7 @@ try {
6
6
  } catch {
7
7
  // ignore errors
8
8
  }
9
+
9
10
  import { cli } from './cli';
10
11
 
11
12
  cli(process.argv.slice(2)[0] || '');
package/src/core.ts CHANGED
@@ -8,7 +8,7 @@ import type { ImportMap, ScopesMap, SpecifierMap } from '@esmx/import';
8
8
 
9
9
  import serialize from 'serialize-javascript';
10
10
  import { type App, createApp } from './app';
11
- import { type ManifestJson, getManifestList } from './manifest-json';
11
+ import { getManifestList, type ManifestJson } from './manifest-json';
12
12
  import {
13
13
  type ModuleConfig,
14
14
  type ParsedModuleConfig,
@@ -19,9 +19,13 @@ import {
19
19
  type ParsedPackConfig,
20
20
  parsePackConfig
21
21
  } from './pack-config';
22
- import type { ImportmapMode } from './render-context';
23
- import type { RenderContext, RenderContextOptions } from './render-context';
22
+ import type {
23
+ ImportmapMode,
24
+ RenderContext,
25
+ RenderContextOptions
26
+ } from './render-context';
24
27
  import { type CacheHandle, createCache } from './utils/cache';
28
+ import { generateSizeReport } from './utils/file-size-stats';
25
29
  import { createClientImportMap, createImportMap } from './utils/import-map';
26
30
  import type { Middleware } from './utils/middleware';
27
31
  import { type ProjectPath, resolvePath } from './utils/resolve-path';
@@ -1079,6 +1083,24 @@ document.head.appendChild(script);
1079
1083
  }
1080
1084
  );
1081
1085
  }
1086
+ /**
1087
+ * Generate bundle size analysis report for the build artifacts.
1088
+ *
1089
+ * @returns {{ text: string, json: object }} Report with text and JSON formats
1090
+ *
1091
+ * @example
1092
+ * ```ts
1093
+ * const report = esmx.generateSizeReport();
1094
+ * console.log(report.text);
1095
+ * console.log(`Total files: ${report.json.totalFiles}`);
1096
+ * ```
1097
+ */
1098
+ public generateSizeReport() {
1099
+ return generateSizeReport(
1100
+ this.resolvePath('dist'),
1101
+ '{client,server,node}/**/!(.*)'
1102
+ );
1103
+ }
1082
1104
  }
1083
1105
 
1084
1106
  /**
package/src/index.ts CHANGED
@@ -1,45 +1,45 @@
1
+ export { type App, createApp } from './app';
1
2
  export {
2
- type EsmxOptions,
3
- type COMMAND,
4
3
  type BuildEnvironment,
4
+ type COMMAND,
5
+ Esmx,
6
+ type EsmxOptions,
5
7
  type ImportMap,
6
- type SpecifierMap,
7
8
  type ScopesMap,
8
- Esmx
9
+ type SpecifierMap
9
10
  } from './core';
11
+ export type {
12
+ ManifestJson,
13
+ ManifestJsonChunk,
14
+ ManifestJsonChunks,
15
+ ManifestJsonExport,
16
+ ManifestJsonExports
17
+ } from './manifest-json';
10
18
  export type {
11
19
  ModuleConfig,
12
- ModuleConfigImportMapping,
13
- ModuleConfigExportExports,
14
20
  ModuleConfigExportExport,
21
+ ModuleConfigExportExports,
15
22
  ModuleConfigExportObject,
23
+ ModuleConfigImportMapping,
16
24
  ParsedModuleConfig,
17
- ParsedModuleConfigExports,
18
- ParsedModuleConfigExport,
19
25
  ParsedModuleConfigEnvironment,
26
+ ParsedModuleConfigExport,
27
+ ParsedModuleConfigExports,
20
28
  ParsedModuleConfigLink
21
29
  } from './module-config';
22
30
  export type {
23
31
  PackConfig,
24
32
  ParsedPackConfig
25
33
  } from './pack-config';
26
- export { type App, createApp } from './app';
27
34
  export {
35
+ RenderContext,
28
36
  type RenderContextOptions,
29
- type ServerRenderHandle,
30
37
  type RenderFiles,
31
- RenderContext
38
+ type ServerRenderHandle
32
39
  } from './render-context';
33
40
  export {
41
+ createMiddleware,
34
42
  isImmutableFile,
35
43
  type Middleware,
36
- createMiddleware,
37
44
  mergeMiddlewares
38
45
  } from './utils/middleware';
39
- export type {
40
- ManifestJson,
41
- ManifestJsonChunk,
42
- ManifestJsonChunks,
43
- ManifestJsonExport,
44
- ManifestJsonExports
45
- } from './manifest-json';
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import {
4
- type ModuleConfig,
5
4
  addPackageExportsToScopes,
6
5
  createDefaultExports,
7
6
  getEnvironmentExports,
@@ -9,8 +8,9 @@ import {
9
8
  getEnvironmentScopes,
10
9
  getEnvironments,
11
10
  getLinks,
12
- parseModuleConfig,
11
+ type ModuleConfig,
13
12
  parsedExportValue,
13
+ parseModuleConfig,
14
14
  processExportArray,
15
15
  processObjectExport,
16
16
  processStringExport,
@@ -0,0 +1,212 @@
1
+ import fs from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { assert, describe, test } from 'vitest';
5
+ import {
6
+ analyzeDirectory,
7
+ formatSize,
8
+ generateSizeReport,
9
+ generateTextReport
10
+ } from './file-size-stats';
11
+
12
+ describe('formatSize', () => {
13
+ test('should format bytes correctly', () => {
14
+ assert.equal(formatSize(0), '0 B');
15
+ assert.equal(formatSize(512), '512 B');
16
+ assert.equal(formatSize(1024), '1.0 KB');
17
+ assert.equal(formatSize(1536), '1.5 KB');
18
+ assert.equal(formatSize(1048576), '1.0 MB');
19
+ assert.equal(formatSize(1073741824), '1.0 GB');
20
+ });
21
+ });
22
+
23
+ describe('analyzeDirectory', () => {
24
+ test('should throw error for non-existent directory', () => {
25
+ try {
26
+ analyzeDirectory('/non-existent-path');
27
+ assert.fail('Should have thrown an error');
28
+ } catch (error) {
29
+ assert.equal(
30
+ (error as Error).message,
31
+ 'Directory does not exist: /non-existent-path'
32
+ );
33
+ }
34
+ });
35
+
36
+ test('should throw error for file path', () => {
37
+ const tempFile = path.join(tmpdir(), 'test-file.txt');
38
+ fs.writeFileSync(tempFile, 'test content');
39
+
40
+ try {
41
+ try {
42
+ analyzeDirectory(tempFile);
43
+ assert.fail('Should have thrown an error');
44
+ } catch (error) {
45
+ assert.equal(
46
+ (error as Error).message,
47
+ `Path is not a directory: ${tempFile}`
48
+ );
49
+ }
50
+ } finally {
51
+ fs.unlinkSync(tempFile);
52
+ }
53
+ });
54
+
55
+ test('should analyze empty directory', () => {
56
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'test-'));
57
+
58
+ try {
59
+ const report = analyzeDirectory(tempDir);
60
+
61
+ assert.equal(report.totalFiles, 0);
62
+ assert.equal(report.totalSize, 0);
63
+ assert.equal(report.totalGzipSize, 0);
64
+ assert.equal(report.compressionRatio, 0);
65
+ assert.deepEqual(report.files, []);
66
+ assert.deepEqual(report.byExtension, {});
67
+ } finally {
68
+ fs.rmSync(tempDir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ test('should analyze directory with files', () => {
73
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'test-'));
74
+
75
+ try {
76
+ const jsContent = 'console.log("hello world");'.repeat(100);
77
+ const cssContent = 'body { color: red; }'.repeat(50);
78
+ const jsonContent = JSON.stringify({ test: 'data' });
79
+
80
+ fs.writeFileSync(path.join(tempDir, 'test.js'), jsContent);
81
+ fs.writeFileSync(path.join(tempDir, 'style.css'), cssContent);
82
+ fs.writeFileSync(path.join(tempDir, 'data.json'), jsonContent);
83
+
84
+ const subDir = path.join(tempDir, 'subdir');
85
+ fs.mkdirSync(subDir);
86
+ fs.writeFileSync(path.join(subDir, 'nested.js'), jsContent);
87
+
88
+ const report = analyzeDirectory(tempDir);
89
+
90
+ assert.equal(report.totalFiles, 4);
91
+ assert.ok(report.totalSize > 0);
92
+ assert.ok(report.totalGzipSize > 0);
93
+ assert.ok(report.compressionRatio >= 0);
94
+
95
+ assert.ok(report.byExtension['.js']);
96
+ assert.ok(report.byExtension['.css']);
97
+ assert.ok(report.byExtension['.json']);
98
+
99
+ assert.equal(report.byExtension['.js'].count, 2);
100
+ assert.equal(report.byExtension['.css'].count, 1);
101
+ assert.equal(report.byExtension['.json'].count, 1);
102
+
103
+ assert.equal(report.files.length, 4);
104
+
105
+ const sizes = report.files.map((f) => f.size);
106
+ assert.deepEqual(
107
+ sizes,
108
+ [...sizes].sort((a, b) => b - a)
109
+ );
110
+ } finally {
111
+ fs.rmSync(tempDir, { recursive: true, force: true });
112
+ }
113
+ });
114
+ });
115
+
116
+ describe('generateTextReport', () => {
117
+ test('should generate text report', () => {
118
+ const report = {
119
+ totalFiles: 2,
120
+ totalSize: 2048,
121
+ totalGzipSize: 1024,
122
+ compressionRatio: 50,
123
+ files: [
124
+ {
125
+ path: '/test/large.js',
126
+ relativePath: 'large.js',
127
+ size: 1536,
128
+ gzipSize: 768,
129
+ ext: '.js'
130
+ },
131
+ {
132
+ path: '/test/small.css',
133
+ relativePath: 'small.css',
134
+ size: 512,
135
+ gzipSize: 256,
136
+ ext: '.css'
137
+ }
138
+ ],
139
+ byExtension: {
140
+ '.js': {
141
+ count: 1,
142
+ totalSize: 1536,
143
+ totalGzipSize: 768,
144
+ avgSize: 1536,
145
+ avgGzipSize: 768
146
+ },
147
+ '.css': {
148
+ count: 1,
149
+ totalSize: 512,
150
+ totalGzipSize: 256,
151
+ avgSize: 512,
152
+ avgGzipSize: 256
153
+ }
154
+ }
155
+ };
156
+
157
+ const text = generateTextReport(report);
158
+
159
+ assert.ok(text.includes('📊 Bundle Analysis'));
160
+ assert.ok(text.includes('large.js'));
161
+ assert.ok(text.includes('small.css'));
162
+ assert.ok(text.includes('Total: 2 files'));
163
+ });
164
+ });
165
+
166
+ describe('generateSizeReport', () => {
167
+ test('should generate report with text and json properties', () => {
168
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'test-'));
169
+
170
+ try {
171
+ fs.writeFileSync(
172
+ path.join(tempDir, 'test.js'),
173
+ 'console.log("test");'
174
+ );
175
+
176
+ const report = generateSizeReport(tempDir);
177
+
178
+ assert.ok(typeof report === 'object');
179
+ assert.ok(typeof report.text === 'string');
180
+ assert.ok(typeof report.json === 'object');
181
+ assert.ok(report.text.includes('📊 Bundle Analysis'));
182
+ assert.ok(!report.text.startsWith('{'));
183
+ assert.ok(report.json.totalFiles !== undefined);
184
+ } finally {
185
+ fs.rmSync(tempDir, { recursive: true, force: true });
186
+ }
187
+ });
188
+
189
+ test('should generate report with pattern filter', () => {
190
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'test-'));
191
+
192
+ try {
193
+ fs.writeFileSync(
194
+ path.join(tempDir, 'test.js'),
195
+ 'console.log("test");'
196
+ );
197
+ fs.writeFileSync(
198
+ path.join(tempDir, 'test.css'),
199
+ 'body { color: red; }'
200
+ );
201
+
202
+ // Test with pattern that only matches .js files
203
+ const report = generateSizeReport(tempDir, '*.js');
204
+
205
+ assert.ok(typeof report === 'object');
206
+ assert.ok(report.json.totalFiles === 1);
207
+ assert.ok(report.json.files[0].ext === '.js');
208
+ } finally {
209
+ fs.rmSync(tempDir, { recursive: true, force: true });
210
+ }
211
+ });
212
+ });
@@ -0,0 +1,189 @@
1
+ import fs, { globSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { gzipSync } from 'node:zlib';
4
+
5
+ export interface FileInfo {
6
+ path: string;
7
+ relativePath: string;
8
+ size: number;
9
+ gzipSize: number;
10
+ ext: string;
11
+ }
12
+
13
+ export interface SizeStatsReport {
14
+ totalFiles: number;
15
+ totalSize: number;
16
+ totalGzipSize: number;
17
+ compressionRatio: number;
18
+ files: FileInfo[];
19
+ byExtension: Record<
20
+ string,
21
+ {
22
+ count: number;
23
+ totalSize: number;
24
+ totalGzipSize: number;
25
+ avgSize: number;
26
+ avgGzipSize: number;
27
+ }
28
+ >;
29
+ }
30
+
31
+ function getGzipSize(filePath: string): number {
32
+ try {
33
+ const content = fs.readFileSync(filePath);
34
+ const compressed = gzipSync(content, { level: 9 });
35
+ return compressed.length;
36
+ } catch (error) {
37
+ return fs.statSync(filePath).size;
38
+ }
39
+ }
40
+
41
+ function getAllFiles(dirPath: string, pattern = '**/!(.*)'): string[] {
42
+ const files = globSync(pattern, {
43
+ cwd: dirPath
44
+ });
45
+
46
+ return files
47
+ .map((relativePath) => path.resolve(dirPath, relativePath))
48
+ .filter((filePath) => fs.statSync(filePath).isFile());
49
+ }
50
+
51
+ export function analyzeDirectory(
52
+ dirPath: string,
53
+ pattern?: string
54
+ ): SizeStatsReport {
55
+ if (!fs.existsSync(dirPath)) {
56
+ throw new Error(`Directory does not exist: ${dirPath}`);
57
+ }
58
+
59
+ const stat = fs.statSync(dirPath);
60
+ if (!stat.isDirectory()) {
61
+ throw new Error(`Path is not a directory: ${dirPath}`);
62
+ }
63
+
64
+ const files = getAllFiles(dirPath, pattern);
65
+ const fileInfos: FileInfo[] = [];
66
+ const byExtension: Record<
67
+ string,
68
+ {
69
+ count: number;
70
+ totalSize: number;
71
+ totalGzipSize: number;
72
+ avgSize: number;
73
+ avgGzipSize: number;
74
+ }
75
+ > = {};
76
+
77
+ let totalSize = 0;
78
+ let totalGzipSize = 0;
79
+
80
+ for (const filePath of files) {
81
+ const fileStat = fs.statSync(filePath);
82
+ const size = fileStat.size;
83
+ const gzipSize = getGzipSize(filePath);
84
+ const relativePath = path.relative(process.cwd(), filePath);
85
+ const ext = path.extname(filePath).toLowerCase() || '(no ext)';
86
+
87
+ const fileInfo: FileInfo = {
88
+ path: filePath,
89
+ relativePath,
90
+ size,
91
+ gzipSize,
92
+ ext
93
+ };
94
+
95
+ fileInfos.push(fileInfo);
96
+ totalSize += size;
97
+ totalGzipSize += gzipSize;
98
+
99
+ if (!byExtension[ext]) {
100
+ byExtension[ext] = {
101
+ count: 0,
102
+ totalSize: 0,
103
+ totalGzipSize: 0,
104
+ avgSize: 0,
105
+ avgGzipSize: 0
106
+ };
107
+ }
108
+
109
+ byExtension[ext].count++;
110
+ byExtension[ext].totalSize += size;
111
+ byExtension[ext].totalGzipSize += gzipSize;
112
+ }
113
+
114
+ Object.values(byExtension).forEach((group) => {
115
+ group.avgSize = Math.round(group.totalSize / group.count);
116
+ group.avgGzipSize = Math.round(group.totalGzipSize / group.count);
117
+ });
118
+
119
+ fileInfos.sort((a, b) => b.size - a.size);
120
+
121
+ const compressionRatio =
122
+ totalSize > 0 ? ((totalSize - totalGzipSize) / totalSize) * 100 : 0;
123
+
124
+ return {
125
+ totalFiles: files.length,
126
+ totalSize,
127
+ totalGzipSize,
128
+ compressionRatio: Math.round(compressionRatio * 100) / 100,
129
+ files: fileInfos,
130
+ byExtension
131
+ };
132
+ }
133
+
134
+ export function formatSize(bytes: number): string {
135
+ const units = ['B', 'KB', 'MB', 'GB'];
136
+ let size = bytes;
137
+ let unitIndex = 0;
138
+
139
+ while (size >= 1024 && unitIndex < units.length - 1) {
140
+ size /= 1024;
141
+ unitIndex++;
142
+ }
143
+
144
+ return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
145
+ }
146
+
147
+ export function generateTextReport(report: SizeStatsReport): string {
148
+ const lines: string[] = [];
149
+
150
+ lines.push('📊 Bundle Analysis');
151
+ lines.push('='.repeat(50));
152
+ lines.push('');
153
+
154
+ const maxPathLength = Math.max(
155
+ ...report.files.map((f) => f.relativePath.length)
156
+ );
157
+ const sizeHeader = 'Size'.padStart(10);
158
+ const gzippedHeader = 'Gzipped'.padStart(10);
159
+ const header = `File${' '.repeat(maxPathLength - 4)} ${sizeHeader} ${gzippedHeader}`;
160
+ lines.push(header);
161
+ lines.push('-'.repeat(header.length));
162
+
163
+ for (const file of report.files) {
164
+ const paddedPath = file.relativePath.padEnd(maxPathLength);
165
+ const sizeStr = formatSize(file.size).padStart(10);
166
+ const gzipStr = formatSize(file.gzipSize).padStart(10);
167
+ lines.push(`${paddedPath} ${sizeStr} ${gzipStr}`);
168
+ }
169
+
170
+ lines.push('');
171
+ lines.push(
172
+ `Total: ${report.totalFiles} files, ${formatSize(report.totalSize)} (gzipped: ${formatSize(report.totalGzipSize)})`
173
+ );
174
+
175
+ return lines.join('\n');
176
+ }
177
+
178
+ export function generateJsonReport(report: SizeStatsReport): string {
179
+ return JSON.stringify(report, null, 2);
180
+ }
181
+
182
+ export function generateSizeReport(dirPath: string, pattern?: string) {
183
+ const json = analyzeDirectory(dirPath, pattern);
184
+
185
+ return {
186
+ text: generateTextReport(json),
187
+ json
188
+ };
189
+ }
@@ -1,4 +1,5 @@
1
1
  import { assert, describe, test } from 'vitest';
2
+ import type { GetImportMapOptions, ImportMapManifest } from './import-map';
2
3
  import {
3
4
  compressImportMap,
4
5
  createImportMap,
@@ -6,7 +7,6 @@ import {
6
7
  createScopesMap,
7
8
  fixImportMapNestedScopes
8
9
  } from './import-map';
9
- import type { GetImportMapOptions, ImportMapManifest } from './import-map';
10
10
 
11
11
  describe('createImportsMap', () => {
12
12
  test('should return empty object for empty manifests', () => {
@@ -1,6 +1,5 @@
1
- import { pathWithoutIndex } from './path-without-index';
2
-
3
1
  import type { ImportMap, ScopesMap, SpecifierMap } from '@esmx/import';
2
+ import { pathWithoutIndex } from './path-without-index';
4
3
 
5
4
  export interface ImportMapManifest {
6
5
  name: string;
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import path from 'node:path';
3
2
  import send from 'send';
4
3
  import type { Esmx } from '../core';
5
4
 
@@ -2,9 +2,8 @@ import type fs from 'node:fs';
2
2
  import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import type { ImportMap, SpecifierMap } from '@esmx/import';
5
- import type { ParsedModuleConfig } from '../module-config';
6
-
7
5
  import * as esmLexer from 'es-module-lexer';
6
+ import type { ParsedModuleConfig } from '../module-config';
8
7
 
9
8
  /**
10
9
  * Get the list of statically imported module names from JS code. Maybe cannot handle multiple concurrent calls, not tested.