@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 +17 -2
- package/dist/core.mjs +19 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +2 -2
- package/dist/module-config.test.mjs +1 -1
- package/dist/utils/file-size-stats.d.ts +29 -0
- package/dist/utils/file-size-stats.mjs +120 -0
- package/dist/utils/file-size-stats.test.d.ts +1 -0
- package/dist/utils/file-size-stats.test.mjs +183 -0
- package/package.json +5 -5
- package/src/app.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/core.ts +25 -3
- package/src/index.ts +19 -19
- package/src/module-config.test.ts +2 -2
- package/src/utils/file-size-stats.test.ts +212 -0
- package/src/utils/file-size-stats.ts +189 -0
- package/src/utils/import-map.test.ts +1 -1
- package/src/utils/import-map.ts +1 -2
- package/src/utils/middleware.ts +0 -1
- package/src/utils/static-import-lexer.ts +1 -2
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
|
|
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";
|
|
@@ -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.
|
|
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": "
|
|
70
|
+
"@biomejs/biome": "2.3.7",
|
|
71
71
|
"@types/find": "^0.2.4",
|
|
72
|
-
"@types/node": "^24.
|
|
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.
|
|
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": "
|
|
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
|
|
9
|
+
import { createMiddleware, type Middleware } from './utils/middleware';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Application instance interface.
|
package/src/cli/index.ts
CHANGED
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
|
|
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 {
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', () => {
|
package/src/utils/import-map.ts
CHANGED
package/src/utils/middleware.ts
CHANGED
|
@@ -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.
|