@module-federation/treeshake-frontend 0.0.1

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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Treeshake Shared Bundling 可视化页面
2
+
3
+ 这是一个前端应用,用于可视化展示一个支持 Tree Shaking 的模块联邦(Module Federation)`shared` 产物打包服务所带来的收益。用户可以通过该页面,直观地对比完整 `shared` 包与经过按需保留导出后 Tree Shaken 的包之间在体积、模块数量和代码内容上的差异。
4
+
5
+ ## 功能特性
6
+
7
+ - **优雅的输入表单**:支持填写 `shared` 名称、版本号,并通过 Chip/Tag 的形式轻松管理需要保留的导出列表。
8
+ - **高级构建配置**:可展开的侧边栏或面板,允许用户精细调整构建参数,如目标环境、产物格式、是否压缩、排除的外部依赖等。所有配置都会自动持久化到本地存储(localStorage)。
9
+ - **Mock Mode**:内置模拟模式开关。在无法连接到真实后端打包服务时,开启 Mock Mode 可以在本地模拟一次完整的 API 响应,从而无需后端也能完整地体验整个应用的核心交互和数据可视化流程。
10
+ - **收益可视化看板**:
11
+ - **核心指标对比**:通过动画数字和进度条,动态展示完整 bundle 与 Tree Shaken bundle 的体积(KB)、节省的体积及百分比。
12
+ - **模块分析**:清晰列出两个 bundle 分别包含的导出模块名和模块数量。
13
+ - **代码审查器**:并排展示两个 bundle 的 JavaScript 代码,支持语法高亮、一键复制和下载到本地。
14
+ - **伪代码加载示例**:提供一段清晰的伪代码,解释如何在不同环境(浏览器、Node.js)中加载并使用打包服务返回的 JS 产物,帮助用户理解其工作原理。
15
+ - **现代化的 UI/UX**:采用 Glassmorphism(玻璃拟物)、流畅的动画、微交互和响应式设计,确保在桌面和移动设备上都有出色的视觉和操作体验。
16
+ - **浅色/深色主题**:支持一键切换浅色和深色模式,并根据系统偏好自动初始化主题。
17
+
18
+ ## 如何使用
19
+
20
+ ### 1. 设置 API Base URL
21
+
22
+ 本应用通过一个后端服务来获取打包产物。你需要配置该服务的地址,有以下三种方式(优先级从高到低):
23
+
24
+ 1. **在页面中配置**:
25
+ - 在主界面的表单区域,找到并展开 “**高级构建配置**” 面板。
26
+ - 在 “**Server API Base URL**” 输入框中,填入你的后端服务地址(例如:`http://localhost:3000/tree-shaking-shared`)。
27
+ - 该配置会自动保存到浏览器的 localStorage(`treeshake_server_url`)中。
28
+
29
+ 2. **通过环境变量配置**:
30
+ - 在项目根目录下创建一个 `.env` 文件。
31
+ - 在文件中添加一行:`VITE_API_BASE_URL=http://localhost:3000/tree-shaking-shared`
32
+ - 重新启动或构建前端应用即可生效。
33
+
34
+ 3. **默认地址**:
35
+ - 如果以上两种方式都未配置,应用将默认尝试连接 `http://localhost:3000/tree-shaking-shared`。
36
+
37
+ ### 2. 使用 Mock Mode(无需后端)
38
+
39
+ 如果你暂时没有可用的后端服务,可以开启页面右上角的 **Mock Mode** 开关。
40
+
41
+ 在此模式下,应用不会发起真实的网络请求,而是使用一个内置的、结构逼真的模拟数据来驱动整个结果展示流程。这对于纯前端开发、UI 调试或功能演示非常有用。
42
+
43
+ ### 3. 调整高级构建配置
44
+
45
+ 在 “**高级构建配置**” 面板中,你可以自定义传递给后端打包服务的构建参数,例如:
46
+
47
+ - `target`、`format`、`platform`:用于 esbuild 或其他打包工具的配置。
48
+ - `minify`:是否开启代码压缩。
49
+ - `External 依赖`:需要从打包产物中排除的第三方库(例如 `react`, `react-dom`)。
50
+ - `请求头 (JSON)`:允许你添加自定义的 HTTP 请求头,例如用于身份验证的 `Authorization`。
51
+ - `额外构建配置 (JSON)`:一个灵活的 JSON 输入框,用于传递任何与后端服务约定的其他参数。
52
+
53
+ ## 技术栈
54
+
55
+ - **框架**: Rsbuild + React
56
+ - **UI**: Tailwind CSS + shadcn/ui
57
+ - **图表**: Recharts
58
+ - **状态管理**: React Hooks
59
+ - **图标**: Lucide React
60
+
61
+ ## 开发与构建
62
+
63
+ ```bash
64
+ # 安装依赖
65
+ pnpm install
66
+
67
+ # 启动开发服务器
68
+ pnpm run dev
69
+
70
+ # 构建生产版本
71
+ pnpm run build
72
+
73
+ # 预览生产版本
74
+ pnpm run preview
75
+
76
+ # E2E (rstest + Playwright core)
77
+ pnpm run test:e2e
78
+ ```
79
+
80
+ ## 与服务端集成(嵌入 UI)
81
+
82
+ 该包提供一个可注册到 `@module-federation/treeshake-server` 的前端适配器:
83
+
84
+ ```ts
85
+ import { createTreeshakeFrontendAdapter } from "@module-federation/treeshake-frontend/adapter";
86
+ import { createApp } from "@module-federation/treeshake-server";
87
+
88
+ const app = createApp(
89
+ { objectStore },
90
+ {
91
+ frontendAdapters: [
92
+ createTreeshakeFrontendAdapter({
93
+ basePath: "/tree-shaking",
94
+ distDir: "/path/to/treeshake-frontend/dist",
95
+ }),
96
+ ],
97
+ },
98
+ );
99
+ ```
100
+
101
+ CLI 模式下,`@module-federation/treeshake-server` 会自动注册该前端并以本地文件系统作为输出。
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export type FrontendAdapter = {
5
+ id: string;
6
+ register: (app: { get: (path: string, handler: any) => void }) => void;
7
+ };
8
+
9
+ export type TreeshakeFrontendAdapterOptions = {
10
+ basePath?: string;
11
+ distDir?: string;
12
+ indexFile?: string;
13
+ spaFallback?: boolean;
14
+ };
15
+
16
+ const defaultBasePath = '/tree-shaking';
17
+
18
+ const contentTypeByExt = (filePath: string) => {
19
+ if (filePath.endsWith('.html')) return 'text/html; charset=utf-8';
20
+ if (filePath.endsWith('.js')) return 'application/javascript';
21
+ if (filePath.endsWith('.css')) return 'text/css';
22
+ if (filePath.endsWith('.json')) return 'application/json';
23
+ if (filePath.endsWith('.svg')) return 'image/svg+xml';
24
+ if (filePath.endsWith('.png')) return 'image/png';
25
+ if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg'))
26
+ return 'image/jpeg';
27
+ if (filePath.endsWith('.webp')) return 'image/webp';
28
+ if (filePath.endsWith('.ico')) return 'image/x-icon';
29
+ return 'application/octet-stream';
30
+ };
31
+
32
+ const resolveDistDir = (override?: string) => {
33
+ if (override) return override;
34
+ const candidate = path.resolve(__dirname, '..');
35
+ const candidateIndex = path.join(candidate, 'index.html');
36
+ if (fs.existsSync(candidateIndex)) return candidate;
37
+ return path.resolve(__dirname, '..', '..', 'dist');
38
+ };
39
+
40
+ const safeResolve = (rootDir: string, requestPath: string) => {
41
+ const rootResolved = path.resolve(rootDir);
42
+ const rel = requestPath.replace(/^\/+/, '');
43
+ const filePath = path.resolve(rootResolved, rel);
44
+ if (
45
+ filePath !== rootResolved &&
46
+ !filePath.startsWith(`${rootResolved}${path.sep}`)
47
+ ) {
48
+ return null;
49
+ }
50
+ return filePath;
51
+ };
52
+
53
+ export function createTreeshakeFrontendAdapter(
54
+ opts: TreeshakeFrontendAdapterOptions = {},
55
+ ): FrontendAdapter {
56
+ const basePath = opts.basePath ?? defaultBasePath;
57
+ const normalizedBase = basePath === '/' ? '' : basePath.replace(/\/$/, '');
58
+ const distDir = resolveDistDir(opts.distDir);
59
+ const indexFile = opts.indexFile ?? 'index.html';
60
+ const spaFallback = opts.spaFallback ?? true;
61
+
62
+ const handler = async (c: any) => {
63
+ let requestPath = c.req.path;
64
+ try {
65
+ requestPath = decodeURIComponent(requestPath);
66
+ } catch {
67
+ return c.text('Not Found', 404);
68
+ }
69
+
70
+ let relPath = requestPath;
71
+ if (normalizedBase && requestPath.startsWith(normalizedBase)) {
72
+ relPath = requestPath.slice(normalizedBase.length);
73
+ }
74
+ if (!relPath || relPath === '/') {
75
+ relPath = `/${indexFile}`;
76
+ }
77
+
78
+ const filePath = safeResolve(distDir, relPath);
79
+ if (filePath) {
80
+ try {
81
+ const stat = await fs.promises.stat(filePath);
82
+ if (stat.isFile()) {
83
+ const buf = await fs.promises.readFile(filePath);
84
+ return new Response(buf, {
85
+ status: 200,
86
+ headers: { 'Content-Type': contentTypeByExt(filePath) },
87
+ });
88
+ }
89
+ } catch {
90
+ // fall through to spa fallback
91
+ }
92
+ }
93
+
94
+ if (!spaFallback) {
95
+ return c.text('Not Found', 404);
96
+ }
97
+
98
+ const fallbackPath = safeResolve(distDir, `/${indexFile}`);
99
+ if (!fallbackPath) {
100
+ return c.text('Not Found', 404);
101
+ }
102
+ try {
103
+ const buf = await fs.promises.readFile(fallbackPath);
104
+ return new Response(buf, {
105
+ status: 200,
106
+ headers: { 'Content-Type': contentTypeByExt(fallbackPath) },
107
+ });
108
+ } catch {
109
+ return c.text('Not Found', 404);
110
+ }
111
+ };
112
+
113
+ return {
114
+ id: 'treeshake-frontend',
115
+ register(app) {
116
+ const base = normalizedBase || '/';
117
+ app.get(base, handler);
118
+ app.get(`${base}/*`, handler);
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,13 @@
1
+ export type FrontendAdapter = {
2
+ id: string;
3
+ register: (app: {
4
+ get: (path: string, handler: any) => void;
5
+ }) => void;
6
+ };
7
+ export type TreeshakeFrontendAdapterOptions = {
8
+ basePath?: string;
9
+ distDir?: string;
10
+ indexFile?: string;
11
+ spaFallback?: boolean;
12
+ };
13
+ export declare function createTreeshakeFrontendAdapter(opts?: TreeshakeFrontendAdapterOptions): FrontendAdapter;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.n = (module)=>{
5
+ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
6
+ __webpack_require__.d(getter, {
7
+ a: getter
8
+ });
9
+ return getter;
10
+ };
11
+ })();
12
+ (()=>{
13
+ __webpack_require__.d = (exports1, definition)=>{
14
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
15
+ enumerable: true,
16
+ get: definition[key]
17
+ });
18
+ };
19
+ })();
20
+ (()=>{
21
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
22
+ })();
23
+ (()=>{
24
+ __webpack_require__.r = (exports1)=>{
25
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
26
+ value: 'Module'
27
+ });
28
+ Object.defineProperty(exports1, '__esModule', {
29
+ value: true
30
+ });
31
+ };
32
+ })();
33
+ var __webpack_exports__ = {};
34
+ __webpack_require__.r(__webpack_exports__);
35
+ __webpack_require__.d(__webpack_exports__, {
36
+ createTreeshakeFrontendAdapter: ()=>createTreeshakeFrontendAdapter
37
+ });
38
+ const external_node_fs_namespaceObject = require("node:fs");
39
+ var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
40
+ const external_node_path_namespaceObject = require("node:path");
41
+ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
42
+ const defaultBasePath = '/tree-shaking';
43
+ const contentTypeByExt = (filePath)=>{
44
+ if (filePath.endsWith('.html')) return 'text/html; charset=utf-8';
45
+ if (filePath.endsWith('.js')) return "application/javascript";
46
+ if (filePath.endsWith('.css')) return 'text/css';
47
+ if (filePath.endsWith('.json')) return 'application/json';
48
+ if (filePath.endsWith('.svg')) return 'image/svg+xml';
49
+ if (filePath.endsWith('.png')) return 'image/png';
50
+ if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
51
+ if (filePath.endsWith('.webp')) return 'image/webp';
52
+ if (filePath.endsWith('.ico')) return 'image/x-icon';
53
+ return 'application/octet-stream';
54
+ };
55
+ const resolveDistDir = (override)=>{
56
+ if (override) return override;
57
+ const candidate = external_node_path_default().resolve(__dirname, '..');
58
+ const candidateIndex = external_node_path_default().join(candidate, 'index.html');
59
+ if (external_node_fs_default().existsSync(candidateIndex)) return candidate;
60
+ return external_node_path_default().resolve(__dirname, '..', '..', 'dist');
61
+ };
62
+ const safeResolve = (rootDir, requestPath)=>{
63
+ const rootResolved = external_node_path_default().resolve(rootDir);
64
+ const rel = requestPath.replace(/^\/+/, '');
65
+ const filePath = external_node_path_default().resolve(rootResolved, rel);
66
+ if (filePath !== rootResolved && !filePath.startsWith(`${rootResolved}${external_node_path_default().sep}`)) return null;
67
+ return filePath;
68
+ };
69
+ function createTreeshakeFrontendAdapter(opts = {}) {
70
+ const basePath = opts.basePath ?? defaultBasePath;
71
+ const normalizedBase = '/' === basePath ? '' : basePath.replace(/\/$/, '');
72
+ const distDir = resolveDistDir(opts.distDir);
73
+ const indexFile = opts.indexFile ?? 'index.html';
74
+ const spaFallback = opts.spaFallback ?? true;
75
+ const handler = async (c)=>{
76
+ let requestPath = c.req.path;
77
+ try {
78
+ requestPath = decodeURIComponent(requestPath);
79
+ } catch {
80
+ return c.text('Not Found', 404);
81
+ }
82
+ let relPath = requestPath;
83
+ if (normalizedBase && requestPath.startsWith(normalizedBase)) relPath = requestPath.slice(normalizedBase.length);
84
+ if (!relPath || '/' === relPath) relPath = `/${indexFile}`;
85
+ const filePath = safeResolve(distDir, relPath);
86
+ if (filePath) try {
87
+ const stat = await external_node_fs_default().promises.stat(filePath);
88
+ if (stat.isFile()) {
89
+ const buf = await external_node_fs_default().promises.readFile(filePath);
90
+ return new Response(buf, {
91
+ status: 200,
92
+ headers: {
93
+ 'Content-Type': contentTypeByExt(filePath)
94
+ }
95
+ });
96
+ }
97
+ } catch {}
98
+ if (!spaFallback) return c.text('Not Found', 404);
99
+ const fallbackPath = safeResolve(distDir, `/${indexFile}`);
100
+ if (!fallbackPath) return c.text('Not Found', 404);
101
+ try {
102
+ const buf = await external_node_fs_default().promises.readFile(fallbackPath);
103
+ return new Response(buf, {
104
+ status: 200,
105
+ headers: {
106
+ 'Content-Type': contentTypeByExt(fallbackPath)
107
+ }
108
+ });
109
+ } catch {
110
+ return c.text('Not Found', 404);
111
+ }
112
+ };
113
+ return {
114
+ id: 'treeshake-frontend',
115
+ register (app) {
116
+ const base = normalizedBase || '/';
117
+ app.get(base, handler);
118
+ app.get(`${base}/*`, handler);
119
+ }
120
+ };
121
+ }
122
+ exports.createTreeshakeFrontendAdapter = __webpack_exports__.createTreeshakeFrontendAdapter;
123
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
124
+ "createTreeshakeFrontendAdapter"
125
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
126
+ Object.defineProperty(exports, '__esModule', {
127
+ value: true
128
+ });
@@ -0,0 +1,83 @@
1
+ import node_fs from "node:fs";
2
+ import node_path from "node:path";
3
+ const defaultBasePath = '/tree-shaking';
4
+ const contentTypeByExt = (filePath)=>{
5
+ if (filePath.endsWith('.html')) return 'text/html; charset=utf-8';
6
+ if (filePath.endsWith('.js')) return "application/javascript";
7
+ if (filePath.endsWith('.css')) return 'text/css';
8
+ if (filePath.endsWith('.json')) return 'application/json';
9
+ if (filePath.endsWith('.svg')) return 'image/svg+xml';
10
+ if (filePath.endsWith('.png')) return 'image/png';
11
+ if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
12
+ if (filePath.endsWith('.webp')) return 'image/webp';
13
+ if (filePath.endsWith('.ico')) return 'image/x-icon';
14
+ return 'application/octet-stream';
15
+ };
16
+ const resolveDistDir = (override)=>{
17
+ if (override) return override;
18
+ const candidate = node_path.resolve(__dirname, '..');
19
+ const candidateIndex = node_path.join(candidate, 'index.html');
20
+ if (node_fs.existsSync(candidateIndex)) return candidate;
21
+ return node_path.resolve(__dirname, '..', '..', 'dist');
22
+ };
23
+ const safeResolve = (rootDir, requestPath)=>{
24
+ const rootResolved = node_path.resolve(rootDir);
25
+ const rel = requestPath.replace(/^\/+/, '');
26
+ const filePath = node_path.resolve(rootResolved, rel);
27
+ if (filePath !== rootResolved && !filePath.startsWith(`${rootResolved}${node_path.sep}`)) return null;
28
+ return filePath;
29
+ };
30
+ function createTreeshakeFrontendAdapter(opts = {}) {
31
+ const basePath = opts.basePath ?? defaultBasePath;
32
+ const normalizedBase = '/' === basePath ? '' : basePath.replace(/\/$/, '');
33
+ const distDir = resolveDistDir(opts.distDir);
34
+ const indexFile = opts.indexFile ?? 'index.html';
35
+ const spaFallback = opts.spaFallback ?? true;
36
+ const handler = async (c)=>{
37
+ let requestPath = c.req.path;
38
+ try {
39
+ requestPath = decodeURIComponent(requestPath);
40
+ } catch {
41
+ return c.text('Not Found', 404);
42
+ }
43
+ let relPath = requestPath;
44
+ if (normalizedBase && requestPath.startsWith(normalizedBase)) relPath = requestPath.slice(normalizedBase.length);
45
+ if (!relPath || '/' === relPath) relPath = `/${indexFile}`;
46
+ const filePath = safeResolve(distDir, relPath);
47
+ if (filePath) try {
48
+ const stat = await node_fs.promises.stat(filePath);
49
+ if (stat.isFile()) {
50
+ const buf = await node_fs.promises.readFile(filePath);
51
+ return new Response(buf, {
52
+ status: 200,
53
+ headers: {
54
+ 'Content-Type': contentTypeByExt(filePath)
55
+ }
56
+ });
57
+ }
58
+ } catch {}
59
+ if (!spaFallback) return c.text('Not Found', 404);
60
+ const fallbackPath = safeResolve(distDir, `/${indexFile}`);
61
+ if (!fallbackPath) return c.text('Not Found', 404);
62
+ try {
63
+ const buf = await node_fs.promises.readFile(fallbackPath);
64
+ return new Response(buf, {
65
+ status: 200,
66
+ headers: {
67
+ 'Content-Type': contentTypeByExt(fallbackPath)
68
+ }
69
+ });
70
+ } catch {
71
+ return c.text('Not Found', 404);
72
+ }
73
+ };
74
+ return {
75
+ id: 'treeshake-frontend',
76
+ register (app) {
77
+ const base = normalizedBase || '/';
78
+ app.get(base, handler);
79
+ app.get(`${base}/*`, handler);
80
+ }
81
+ };
82
+ }
83
+ export { createTreeshakeFrontendAdapter };
Binary file
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><html><head><link rel="icon" href="/tree-shaking/favicon.ico"><title>Tree Shaking Visualizer</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/tree-shaking/static/js/lib-react.c59642e3.js"></script><script defer src="/tree-shaking/static/js/lib-router.75e1e689.js"></script><script defer src="/tree-shaking/static/js/954.dfe166a3.js"></script><script defer src="/tree-shaking/static/js/index.db4e73c6.js"></script><link href="/tree-shaking/static/css/index.16175e0f.css" rel="stylesheet"></head><body><div id="root"></div></body></html>