@lark-apaas/miaoda-presets 0.1.0-alpha.9 → 0.1.0-alpha.overlay.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/lib/eslint.config.js +1 -1
- package/lib/overlay/components.js +169 -0
- package/lib/overlay/index.js +329 -0
- package/lib/react-refresh-plugin-option.js +12 -0
- package/lib/recommend/eslint.js +29 -1
- package/lib/recommend/rspack.js +108 -45
- package/lib/recommend/tailwind.js +14 -3
- package/lib/rspack-plugins/route-parser-plugin.js +275 -0
- package/lib/rspack-plugins/slardar-performance-monitor-plugin.js +96 -0
- package/lib/rspack.config.js +52 -8
- package/lib/tailwind.config.js +3 -0
- package/package.json +23 -18
package/lib/eslint.config.js
CHANGED
|
@@ -15,8 +15,8 @@ function createEslintConfig() {
|
|
|
15
15
|
return typescript_eslint_1.default.config({ ignores: ['dist', 'node_modules', 'build'] }, {
|
|
16
16
|
extends: [
|
|
17
17
|
js_1.default.configs.recommended,
|
|
18
|
-
eslint_1.default,
|
|
19
18
|
...typescript_eslint_1.default.configs.recommended,
|
|
19
|
+
eslint_1.default,
|
|
20
20
|
],
|
|
21
21
|
files: ['src/**/*.{ts,tsx}'],
|
|
22
22
|
languageOptions: {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 错误容器组件
|
|
4
|
+
* @param {*} document
|
|
5
|
+
* @param {*} root
|
|
6
|
+
* @param {object} props
|
|
7
|
+
* @param {string} props.errorMessage - 错误信息
|
|
8
|
+
* @param {function} props.onRepairClick - 修复按钮点击事件
|
|
9
|
+
* @param {function} props.onCopyClick - 复制按钮点击事件
|
|
10
|
+
*/
|
|
11
|
+
function ErrorContainer(document, root, props) {
|
|
12
|
+
// 创建容器元素
|
|
13
|
+
const container = document.createElement('div');
|
|
14
|
+
container.className = 'container';
|
|
15
|
+
// 创建内容元素
|
|
16
|
+
const content = document.createElement('div');
|
|
17
|
+
content.className = 'content';
|
|
18
|
+
container.appendChild(content);
|
|
19
|
+
// 创建图片元素
|
|
20
|
+
const img = document.createElement('img');
|
|
21
|
+
img.src =
|
|
22
|
+
'https://lf3-static.bytednsdoc.com/obj/eden-cn/ylcylz_fsph_ryhs/ljhwZthlaukjlkulzlp/feisuda/template/render_error.svg';
|
|
23
|
+
img.alt = 'render error';
|
|
24
|
+
// 图片占位
|
|
25
|
+
img.width = 120;
|
|
26
|
+
img.height = 120;
|
|
27
|
+
img.className = 'error-image';
|
|
28
|
+
content.appendChild(img);
|
|
29
|
+
// 创建标题元素
|
|
30
|
+
const title = document.createElement('p');
|
|
31
|
+
title.className = 'title';
|
|
32
|
+
title.textContent = '哎呀,写错代码了';
|
|
33
|
+
content.appendChild(title);
|
|
34
|
+
// 创建描述元素
|
|
35
|
+
const description = document.createElement('p');
|
|
36
|
+
description.className = 'description';
|
|
37
|
+
description.textContent = '可复制错误信息,或告诉妙搭进行修复。';
|
|
38
|
+
content.appendChild(description);
|
|
39
|
+
// 创建按钮组元素
|
|
40
|
+
const buttonGroup = document.createElement('div');
|
|
41
|
+
buttonGroup.className = 'button-group';
|
|
42
|
+
content.appendChild(buttonGroup);
|
|
43
|
+
// 创建复制按钮
|
|
44
|
+
const copyBtn = document.createElement('button');
|
|
45
|
+
copyBtn.className = 'button button-copy';
|
|
46
|
+
copyBtn.id = 'copyBtn';
|
|
47
|
+
copyBtn.textContent = '复制错误信息';
|
|
48
|
+
copyBtn.addEventListener('click', props.onCopyClick);
|
|
49
|
+
buttonGroup.appendChild(copyBtn);
|
|
50
|
+
// 创建修复按钮
|
|
51
|
+
const repairBtn = document.createElement('button');
|
|
52
|
+
repairBtn.className = 'button button-repair';
|
|
53
|
+
repairBtn.id = 'repairBtn';
|
|
54
|
+
repairBtn.textContent = '告诉妙搭修复';
|
|
55
|
+
repairBtn.addEventListener('click', props.onRepairClick);
|
|
56
|
+
buttonGroup.appendChild(repairBtn);
|
|
57
|
+
// 添加到根元素
|
|
58
|
+
root.appendChild(container);
|
|
59
|
+
}
|
|
60
|
+
function RootStyle(document, root) {
|
|
61
|
+
const style = document.createElement('style');
|
|
62
|
+
style.id = 'react-refresh-overlay-style';
|
|
63
|
+
style.textContent = `
|
|
64
|
+
* {
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding: 0;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
body {
|
|
71
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
72
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
73
|
+
sans-serif;
|
|
74
|
+
-webkit-font-smoothing: antialiased;
|
|
75
|
+
-moz-osx-font-smoothing: grayscale;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.container {
|
|
79
|
+
min-height: 100vh;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
background-color: #F3F4F6; /* bg-gray-100 */
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.content {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
align-items: center;
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.error-image {
|
|
95
|
+
margin-bottom: 16px; /* mb-4 */
|
|
96
|
+
width: 120px; /* w-[120px] */
|
|
97
|
+
height: auto;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.title {
|
|
101
|
+
font-size: 18px; /* text-l (推测为 text-lg) */
|
|
102
|
+
line-height: 22px;
|
|
103
|
+
color: #1F2329;
|
|
104
|
+
font-weight: 500; /* font-medium */
|
|
105
|
+
margin-bottom: 8px; /* mb-2 */
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.description {
|
|
109
|
+
font-size: 14px; /* text-sm */
|
|
110
|
+
line-height: 22px;
|
|
111
|
+
color: #646A73;
|
|
112
|
+
font-weight: 400; /* font-normal */
|
|
113
|
+
margin-bottom: 8px; /* mb-2 */
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.button-group {
|
|
117
|
+
display: flex;
|
|
118
|
+
gap: 16px; /* space-x-4 */
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.button {
|
|
122
|
+
height: 32px; /* h-[32px] */
|
|
123
|
+
padding: 0 16px;
|
|
124
|
+
border-radius: 6px; /* rounded-[6px] */
|
|
125
|
+
font-size: 14px; /* text-sm */
|
|
126
|
+
font-weight: 400; /* font-[400] */
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
transition: all 0.2s;
|
|
129
|
+
outline: none; /* focus:outline-hidden */
|
|
130
|
+
border: 1px solid;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.button-copy {
|
|
134
|
+
background-color: white; /* bg-white */
|
|
135
|
+
color: #4B5563; /* text-gray-600 */
|
|
136
|
+
border-color: #D0D3D6; /* border-[#D0D3D6] */
|
|
137
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* shadow-xs */
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.button-copy:hover {
|
|
141
|
+
background-color: #F3F4F6; /* hover:bg-gray-100 */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.button-copy:active {
|
|
145
|
+
background-color: #E5E7EB; /* active:bg-gray-200 */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.button-repair {
|
|
149
|
+
background-color: #2563EB; /* bg-blue-600 */
|
|
150
|
+
color: white; /* text-white */
|
|
151
|
+
border-color: transparent; /* border-transparent */
|
|
152
|
+
font-weight: 500; /* font-medium */
|
|
153
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* shadow-xs */
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.button-repair:hover {
|
|
157
|
+
background-color: #2563EB; /* hover:bg-blue-600 (保持不变) */
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.button-repair:active {
|
|
161
|
+
background-color: #1E40AF; /* active:bg-blue-700 */
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
root.appendChild(style);
|
|
165
|
+
}
|
|
166
|
+
module.exports = {
|
|
167
|
+
ErrorContainer,
|
|
168
|
+
RootStyle,
|
|
169
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
const { ErrorContainer, RootStyle } = require('./components');
|
|
4
|
+
/* ===== Cached elements for DOM manipulations ===== */
|
|
5
|
+
/**
|
|
6
|
+
* The iframe that contains the overlay.
|
|
7
|
+
* @type {HTMLIFrameElement}
|
|
8
|
+
*/
|
|
9
|
+
let iframeRoot = null;
|
|
10
|
+
/**
|
|
11
|
+
* The document object from the iframe root, used to create and render elements.
|
|
12
|
+
* @type {Document}
|
|
13
|
+
*/
|
|
14
|
+
let rootDocument = null;
|
|
15
|
+
/**
|
|
16
|
+
* The root div elements will attach to.
|
|
17
|
+
* @type {HTMLDivElement}
|
|
18
|
+
*/
|
|
19
|
+
let root = null;
|
|
20
|
+
/**
|
|
21
|
+
* A Cached function to allow deferred render.
|
|
22
|
+
* @type {RenderFn | null}
|
|
23
|
+
*/
|
|
24
|
+
let scheduledRenderFn = null;
|
|
25
|
+
/* ===== Overlay State ===== */
|
|
26
|
+
/**
|
|
27
|
+
* The latest error message from Webpack compilation.
|
|
28
|
+
* @type {string}
|
|
29
|
+
*/
|
|
30
|
+
let currentCompileErrorMessage = '';
|
|
31
|
+
/**
|
|
32
|
+
* The latest runtime error objects.
|
|
33
|
+
* @type {Error[]}
|
|
34
|
+
*/
|
|
35
|
+
let currentRuntimeErrors = [];
|
|
36
|
+
/**
|
|
37
|
+
* The render mode the overlay is currently in.
|
|
38
|
+
* @type {'compileError' | 'runtimeError' | null}
|
|
39
|
+
*/
|
|
40
|
+
let currentMode = null;
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} IframeProps
|
|
43
|
+
* @property {function(): void} onIframeLoad
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Creates the main `iframe` the overlay will attach to.
|
|
47
|
+
* Accepts a callback to be ran after iframe is initialized.
|
|
48
|
+
* @param {Document} document
|
|
49
|
+
* @param {HTMLElement} root
|
|
50
|
+
* @param {IframeProps} props
|
|
51
|
+
* @returns {HTMLIFrameElement}
|
|
52
|
+
*/
|
|
53
|
+
function IframeRoot(document, root, props) {
|
|
54
|
+
const iframe = document.createElement('iframe');
|
|
55
|
+
iframe.id = 'react-refresh-overlay';
|
|
56
|
+
iframe.src = 'about:blank';
|
|
57
|
+
iframe.style.border = 'none';
|
|
58
|
+
iframe.style.height = '100%';
|
|
59
|
+
iframe.style.left = '0';
|
|
60
|
+
iframe.style.minHeight = '100vh';
|
|
61
|
+
iframe.style.minHeight = '-webkit-fill-available';
|
|
62
|
+
iframe.style.position = 'fixed';
|
|
63
|
+
iframe.style.top = '0';
|
|
64
|
+
iframe.style.width = '100vw';
|
|
65
|
+
iframe.style.zIndex = '2147483647';
|
|
66
|
+
iframe.addEventListener('load', function onLoad() {
|
|
67
|
+
// Reset margin of iframe body
|
|
68
|
+
iframe.contentDocument.body.style.margin = '0';
|
|
69
|
+
props.onIframeLoad();
|
|
70
|
+
});
|
|
71
|
+
// We skip mounting and returns as we need to ensure
|
|
72
|
+
// the load event is fired after we setup the global variable
|
|
73
|
+
return iframe;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates the main `div` element for the overlay to render.
|
|
77
|
+
* @param {Document} document
|
|
78
|
+
* @param {HTMLElement} root
|
|
79
|
+
* @returns {HTMLDivElement}
|
|
80
|
+
*/
|
|
81
|
+
function OverlayRoot(document, root) {
|
|
82
|
+
const div = document.createElement('div');
|
|
83
|
+
div.id = 'container';
|
|
84
|
+
root.appendChild(div);
|
|
85
|
+
return div;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Ensures the iframe root and the overlay root are both initialized before render.
|
|
89
|
+
* If check fails, render will be deferred until both roots are initialized.
|
|
90
|
+
* @param {RenderFn} renderFn A function that triggers a DOM render.
|
|
91
|
+
* @returns {void}
|
|
92
|
+
*/
|
|
93
|
+
function ensureRootExists(renderFn) {
|
|
94
|
+
if (root) {
|
|
95
|
+
// Overlay root is ready, we can render right away.
|
|
96
|
+
renderFn();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Creating an iframe may be asynchronous so we'll defer render.
|
|
100
|
+
// In case of multiple calls, function from the last call will be used.
|
|
101
|
+
scheduledRenderFn = renderFn;
|
|
102
|
+
if (iframeRoot) {
|
|
103
|
+
// Iframe is already ready, it will fire the load event.
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Create the iframe root, and, the overlay root inside it when it is ready.
|
|
107
|
+
iframeRoot = IframeRoot(document, document.body, {
|
|
108
|
+
onIframeLoad: function onIframeLoad() {
|
|
109
|
+
rootDocument = iframeRoot.contentDocument;
|
|
110
|
+
RootStyle(rootDocument, rootDocument.head);
|
|
111
|
+
root = OverlayRoot(rootDocument, rootDocument.body);
|
|
112
|
+
scheduledRenderFn();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
// We have to mount here to ensure `iframeRoot` is set when `onIframeLoad` fires.
|
|
116
|
+
// This is because onIframeLoad() will be called synchronously
|
|
117
|
+
// or asynchronously depending on the browser.
|
|
118
|
+
document.body.appendChild(iframeRoot);
|
|
119
|
+
}
|
|
120
|
+
function removeAllChildren(element, skip) {
|
|
121
|
+
/** @type {Node[]} */
|
|
122
|
+
const childList = Array.prototype.slice.call(element.childNodes, typeof skip !== 'undefined' ? skip : 0);
|
|
123
|
+
for (let i = 0; i < childList.length; i += 1) {
|
|
124
|
+
element.removeChild(childList[i]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function copyToClipboard(text) {
|
|
128
|
+
try {
|
|
129
|
+
// 优先使用现代的 Clipboard API
|
|
130
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
131
|
+
await navigator.clipboard.writeText(text);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// 降级方案:使用传统的 execCommand 方法
|
|
135
|
+
return fallbackCopyToClipboard(text);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error('复制到剪切板失败:', error);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 获取父窗口 origin
|
|
143
|
+
function getPreviewParentOrigin() {
|
|
144
|
+
// 优先使用 document.referrer
|
|
145
|
+
if (document.referrer) {
|
|
146
|
+
try {
|
|
147
|
+
const url = new URL(document.referrer);
|
|
148
|
+
return url.origin;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
console.error('解析 referrer 失败:', e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 降级方案:使用通配符(不安全,仅用于开发环境)
|
|
155
|
+
return '*';
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Creates the main `div` element for the overlay to render.
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
function render() {
|
|
162
|
+
ensureRootExists(function () {
|
|
163
|
+
removeAllChildren(root);
|
|
164
|
+
// 发送 postMessage
|
|
165
|
+
const sendPostMessage = (message, targetOrigin) => {
|
|
166
|
+
const origin = targetOrigin || getPreviewParentOrigin();
|
|
167
|
+
window.parent.postMessage(message, origin);
|
|
168
|
+
};
|
|
169
|
+
if (currentCompileErrorMessage) {
|
|
170
|
+
currentMode = 'compileError';
|
|
171
|
+
ErrorContainer(rootDocument, root, {
|
|
172
|
+
errorMessage: currentCompileErrorMessage,
|
|
173
|
+
onRepairClick: function onRepairClick() {
|
|
174
|
+
sendPostMessage({
|
|
175
|
+
type: 'RenderErrorRepair',
|
|
176
|
+
data: currentCompileErrorMessage,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
onCopyClick: function onCopyClick() {
|
|
180
|
+
copyToClipboard(currentCompileErrorMessage);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (currentRuntimeErrors.length) {
|
|
185
|
+
currentMode = 'runtimeError';
|
|
186
|
+
let errors = '';
|
|
187
|
+
currentRuntimeErrors.forEach((error) => {
|
|
188
|
+
errors += `${error.message}\n${error.stack}\n`;
|
|
189
|
+
});
|
|
190
|
+
// 叠加全部报错,将报错信息发送到妙搭
|
|
191
|
+
ErrorContainer(rootDocument, root, {
|
|
192
|
+
errorMessage: errors,
|
|
193
|
+
onRepairClick: function onRepairClick() {
|
|
194
|
+
sendPostMessage({
|
|
195
|
+
type: 'RenderErrorRepair',
|
|
196
|
+
data: errors,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
onCopyClick: function onCopyClick() {
|
|
200
|
+
copyToClipboard(errors);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Destroys the state of the overlay.
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
function cleanup() {
|
|
211
|
+
// Clean up and reset all internal state.
|
|
212
|
+
try {
|
|
213
|
+
document.body.removeChild(iframeRoot);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
// In case user render react app directly to body, will trigger `NotFoundError` when recovery from an Error
|
|
217
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild#exceptions
|
|
218
|
+
}
|
|
219
|
+
scheduledRenderFn = null;
|
|
220
|
+
root = null;
|
|
221
|
+
iframeRoot = null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Clears Webpack compilation errors and dismisses the compile error overlay.
|
|
225
|
+
* @returns {void}
|
|
226
|
+
*/
|
|
227
|
+
function clearCompileError() {
|
|
228
|
+
if (!root || currentMode !== 'compileError') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
currentCompileErrorMessage = '';
|
|
232
|
+
currentMode = null;
|
|
233
|
+
cleanup();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Clears runtime error records and dismisses the runtime error overlay.
|
|
237
|
+
* @param {boolean} [dismissOverlay] Whether to dismiss the overlay or not.
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
function clearRuntimeErrors(dismissOverlay) {
|
|
241
|
+
if (!root || currentMode !== 'runtimeError') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
currentRuntimeErrors = [];
|
|
245
|
+
if (typeof dismissOverlay === 'undefined' || dismissOverlay) {
|
|
246
|
+
currentMode = null;
|
|
247
|
+
cleanup();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Shows the compile error overlay with the specific Webpack error message.
|
|
252
|
+
* @param {string} message
|
|
253
|
+
* @returns {void}
|
|
254
|
+
*/
|
|
255
|
+
function showCompileError(message) {
|
|
256
|
+
if (!message) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
currentCompileErrorMessage = message;
|
|
260
|
+
render();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Shows the runtime error overlay with the specific error records.
|
|
264
|
+
* @param {Error[]} errors
|
|
265
|
+
* @returns {void}
|
|
266
|
+
*/
|
|
267
|
+
function showRuntimeErrors(errors) {
|
|
268
|
+
if (!errors || !errors.length) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
currentRuntimeErrors = errors;
|
|
272
|
+
render();
|
|
273
|
+
}
|
|
274
|
+
function debounce(fn, wait) {
|
|
275
|
+
/**
|
|
276
|
+
* A cached setTimeout handler.
|
|
277
|
+
* @type {number | undefined}
|
|
278
|
+
*/
|
|
279
|
+
let timer;
|
|
280
|
+
/**
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
function debounced() {
|
|
284
|
+
const context = this;
|
|
285
|
+
const args = arguments;
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
timer = setTimeout(function () {
|
|
288
|
+
return fn.apply(context, args);
|
|
289
|
+
}, wait);
|
|
290
|
+
}
|
|
291
|
+
return debounced;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The debounced version of `showRuntimeErrors` to prevent frequent renders
|
|
295
|
+
* due to rapid firing listeners.
|
|
296
|
+
* @param {Error[]} errors
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
const debouncedShowRuntimeErrors = debounce(showRuntimeErrors, 30);
|
|
300
|
+
/**
|
|
301
|
+
* Detects if an error is a Webpack compilation error.
|
|
302
|
+
* @param {Error} error The error of interest.
|
|
303
|
+
* @returns {boolean} If the error is a Webpack compilation error.
|
|
304
|
+
*/
|
|
305
|
+
function isWebpackCompileError(error) {
|
|
306
|
+
return (/Module [A-z ]+\(from/.test(error.message) ||
|
|
307
|
+
/Cannot find module/.test(error.message));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Handles runtime error contexts captured with EventListeners.
|
|
311
|
+
* Integrates with a runtime error overlay.
|
|
312
|
+
* @param {Error} error A valid error object.
|
|
313
|
+
* @returns {void}
|
|
314
|
+
*/
|
|
315
|
+
function handleRuntimeError(error) {
|
|
316
|
+
if (error &&
|
|
317
|
+
!isWebpackCompileError(error) &&
|
|
318
|
+
currentRuntimeErrors.indexOf(error) === -1) {
|
|
319
|
+
currentRuntimeErrors = currentRuntimeErrors.concat(error);
|
|
320
|
+
}
|
|
321
|
+
debouncedShowRuntimeErrors(currentRuntimeErrors);
|
|
322
|
+
}
|
|
323
|
+
module.exports = Object.freeze({
|
|
324
|
+
clearCompileError: clearCompileError,
|
|
325
|
+
clearRuntimeErrors: clearRuntimeErrors,
|
|
326
|
+
handleRuntimeError: handleRuntimeError,
|
|
327
|
+
showCompileError: showCompileError,
|
|
328
|
+
showRuntimeErrors: showRuntimeErrors,
|
|
329
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.pluginOption = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
exports.pluginOption = {
|
|
9
|
+
overlay: {
|
|
10
|
+
module: path_1.default.resolve(__dirname, 'overlay/index.js'),
|
|
11
|
+
},
|
|
12
|
+
};
|
package/lib/recommend/eslint.js
CHANGED
|
@@ -18,11 +18,39 @@ exports.default = {
|
|
|
18
18
|
'react/no-unknown-property': 'off', // 允许未知属性
|
|
19
19
|
'react-hooks/exhaustive-deps': 'off', // 不强制检查依赖数组
|
|
20
20
|
// JavaScript 基础规则
|
|
21
|
-
|
|
21
|
+
// 通过 tsc 来控制,默认 AI 不写 js 文件
|
|
22
|
+
'no-undef': 'off', // 禁止使用未声明的变量
|
|
22
23
|
'no-console': 'off', // 允许使用 console
|
|
23
24
|
'prefer-const': 'off', // 不强制使用 const
|
|
24
25
|
// Import 相关检查
|
|
25
26
|
'import/no-unresolved': 'error', // 检查导入路径是否存在
|
|
26
27
|
'import/named': 'error', // 检查命名导入是否存在
|
|
28
|
+
// 效果相关:模型不会
|
|
29
|
+
'no-constant-binary-expression': 'off', // 不强制使用常量二进制表达式
|
|
30
|
+
'no-restricted-syntax': [
|
|
31
|
+
'error',
|
|
32
|
+
// 效果相关:希望模型使用内置 logger 而不是 console
|
|
33
|
+
{
|
|
34
|
+
selector: "CallExpression[callee.object.name='console'][callee.property.name=/^(log|warn|info|debug|trace)$/]",
|
|
35
|
+
message: 'Avoid using console.log, console.warn, etc. Use `@byted/spark-framework/logger` instead.',
|
|
36
|
+
},
|
|
37
|
+
// 禁用BOM方法alert和confirm,及不推荐使用同名方法
|
|
38
|
+
{
|
|
39
|
+
message: "Please don't use window.alert, use `Dialog` component instead for better user experience and consistency",
|
|
40
|
+
selector: "MemberExpression[object.name='window'][property.name='alert']",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
message: "Please don't use window.confirm, use `Dialog` component instead for better user experience and consistency",
|
|
44
|
+
selector: "MemberExpression[object.name='window'][property.name='confirm']",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
message: "Please don't use alert. It may conflict with window.alert BOM method, use `Dialog` component instead for better user experience and consistency",
|
|
48
|
+
selector: "CallExpression[callee.name='alert']",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
message: "Please don't use confirm. It may conflict with window.confirm BOM method, use `Dialog` component instead for better user experience and consistency",
|
|
52
|
+
selector: "CallExpression[callee.name='confirm']",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
27
55
|
},
|
|
28
56
|
};
|
package/lib/recommend/rspack.js
CHANGED
|
@@ -6,11 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createRecommendRspackConfig = createRecommendRspackConfig;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const core_1 = __importDefault(require("@rspack/core"));
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
10
|
+
const RouteParserPlugin = require('../rspack-plugins/route-parser-plugin');
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
|
+
const SlardarPerformanceMonitorPlugin = require('../rspack-plugins/slardar-performance-monitor-plugin');
|
|
9
13
|
// eslint-disable-next-line max-lines-per-function
|
|
10
14
|
function createRecommendRspackConfig(options) {
|
|
11
|
-
const {
|
|
12
|
-
console.log('isDev:', isDev);
|
|
13
|
-
console.log('enableReactRrefresh:', enableReactRrefresh);
|
|
15
|
+
const { enableReactRefresh = false, isDev = true, needRoutes = true, runtimeMode = 'fullstack', } = options;
|
|
14
16
|
return {
|
|
15
17
|
experiments: {
|
|
16
18
|
css: true,
|
|
@@ -37,7 +39,31 @@ function createRecommendRspackConfig(options) {
|
|
|
37
39
|
},
|
|
38
40
|
{
|
|
39
41
|
test: /\.css$/,
|
|
40
|
-
use: [
|
|
42
|
+
use: [
|
|
43
|
+
{
|
|
44
|
+
loader: 'postcss-loader',
|
|
45
|
+
options: {
|
|
46
|
+
postcssOptions: {
|
|
47
|
+
plugins: [
|
|
48
|
+
[
|
|
49
|
+
'postcss-import',
|
|
50
|
+
{
|
|
51
|
+
resolve: (id, _basedir) => {
|
|
52
|
+
// 只有dev环境需要打包选中精调所需的一些预置样式, prod环境则不打包
|
|
53
|
+
if (id === '@/inspector.dev.css') {
|
|
54
|
+
return isDev
|
|
55
|
+
? path_1.default.join(_basedir, '/inspector.dev.css')
|
|
56
|
+
: [];
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
41
67
|
type: 'css',
|
|
42
68
|
},
|
|
43
69
|
{
|
|
@@ -63,7 +89,7 @@ function createRecommendRspackConfig(options) {
|
|
|
63
89
|
}
|
|
64
90
|
: {}),
|
|
65
91
|
development: isDev,
|
|
66
|
-
refresh:
|
|
92
|
+
refresh: enableReactRefresh,
|
|
67
93
|
},
|
|
68
94
|
},
|
|
69
95
|
},
|
|
@@ -91,52 +117,89 @@ function createRecommendRspackConfig(options) {
|
|
|
91
117
|
new core_1.default.DefinePlugin({
|
|
92
118
|
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
|
93
119
|
'process.env.CWD': JSON.stringify(process.cwd()),
|
|
120
|
+
'process.env.runtimeMode': JSON.stringify(runtimeMode),
|
|
94
121
|
}),
|
|
95
122
|
new core_1.default.optimize.LimitChunkCountPlugin({
|
|
96
123
|
maxChunks: 1,
|
|
97
124
|
}),
|
|
125
|
+
// 全栈模式下,增加性能监控上报脚本注入
|
|
126
|
+
runtimeMode === 'fullstack' ? new SlardarPerformanceMonitorPlugin() : undefined,
|
|
127
|
+
// 开发环境下,解析路由
|
|
128
|
+
isDev &&
|
|
129
|
+
needRoutes &&
|
|
130
|
+
new RouteParserPlugin({
|
|
131
|
+
appPath: './client/src/app.tsx',
|
|
132
|
+
outputPath: path_1.default.resolve(__dirname, 'dist/client/routes.json'),
|
|
133
|
+
}),
|
|
98
134
|
],
|
|
99
|
-
optimization: isDev
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
optimization: isDev
|
|
136
|
+
? {}
|
|
137
|
+
: {
|
|
138
|
+
moduleIds: 'deterministic',
|
|
139
|
+
concatenateModules: true,
|
|
140
|
+
minimize: true, // 对应vite的minify配置
|
|
141
|
+
minimizer: [
|
|
142
|
+
new core_1.default.SwcJsMinimizerRspackPlugin({
|
|
143
|
+
minimizerOptions: {
|
|
144
|
+
// 保持不压缩
|
|
145
|
+
minify: !isDev,
|
|
146
|
+
mangle: !isDev,
|
|
147
|
+
format: {
|
|
148
|
+
beautify: isDev,
|
|
149
|
+
comments: false,
|
|
150
|
+
},
|
|
151
|
+
compress: {
|
|
152
|
+
keep_classnames: true,
|
|
153
|
+
keep_fnames: true,
|
|
154
|
+
keep_fargs: !isDev,
|
|
155
|
+
unused: true,
|
|
156
|
+
dead_code: true,
|
|
157
|
+
drop_debugger: true,
|
|
158
|
+
// FIXME: 先临时开始 console.log
|
|
159
|
+
// drop_console: !isDev,
|
|
160
|
+
const_to_let: !isDev,
|
|
161
|
+
booleans_as_integers: !isDev,
|
|
162
|
+
booleans: !isDev,
|
|
163
|
+
// maybe unsafe
|
|
164
|
+
reduce_funcs: !isDev,
|
|
165
|
+
reduce_vars: !isDev,
|
|
166
|
+
},
|
|
128
167
|
},
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
},
|
|
168
|
+
}),
|
|
169
|
+
new core_1.default.LightningCssMinimizerRspackPlugin(),
|
|
170
|
+
],
|
|
171
|
+
sideEffects: true,
|
|
172
|
+
usedExports: true,
|
|
173
|
+
innerGraph: true,
|
|
174
|
+
providedExports: true,
|
|
175
|
+
mergeDuplicateChunks: true,
|
|
176
|
+
splitChunks: false, // 禁用代码分割,保持单文件输出
|
|
177
|
+
},
|
|
140
178
|
devtool: isDev ? 'source-map' : false, // 对应vite的sourcemap配置
|
|
179
|
+
devServer: {
|
|
180
|
+
headers: (req) => {
|
|
181
|
+
// 获取请求的Origin头
|
|
182
|
+
const requestOrigin = req.headers.origin ?? '';
|
|
183
|
+
// 定义允许的域名白名单
|
|
184
|
+
const allowedOrigins = [
|
|
185
|
+
'https://miaoda.feishu.cn',
|
|
186
|
+
'https://miaoda.feishu-boe.cn',
|
|
187
|
+
'https://miaoda.feishu-pre.cn',
|
|
188
|
+
];
|
|
189
|
+
// 检查请求的Origin是否在允许的列表中
|
|
190
|
+
const allowedOrigin = allowedOrigins.includes(requestOrigin)
|
|
191
|
+
? requestOrigin
|
|
192
|
+
: allowedOrigins[0]; // 默认返回第一个允许的域名
|
|
193
|
+
return {
|
|
194
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
client: {
|
|
198
|
+
overlay: {
|
|
199
|
+
errors: false,
|
|
200
|
+
warnings: false,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
141
204
|
};
|
|
142
205
|
}
|
|
@@ -37,12 +37,23 @@ exports.createRecommendTailwindConfig = createRecommendTailwindConfig;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
39
39
|
function createRecommendTailwindConfig(options) {
|
|
40
|
+
// 默认扫描外网包依赖
|
|
41
|
+
const { packageName = '@lark-apaas/client-toolkit' } = options;
|
|
42
|
+
let pkgPath = '';
|
|
43
|
+
if (packageName) {
|
|
44
|
+
try {
|
|
45
|
+
// 扫描依赖中的tailwind样式
|
|
46
|
+
pkgPath = path.join(path.dirname(require.resolve(packageName)), '/**/*.js');
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.warn(`Failed to resolve package ${packageName}, use default path`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
40
52
|
return {
|
|
41
53
|
darkMode: 'class',
|
|
42
54
|
content: [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
],
|
|
55
|
+
pkgPath,
|
|
56
|
+
].filter(Boolean),
|
|
46
57
|
prefix: '',
|
|
47
58
|
plugins: [],
|
|
48
59
|
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { parse } = require('@babel/parser');
|
|
6
|
+
const traverse = require('@babel/traverse').default;
|
|
7
|
+
const t = require('@babel/types');
|
|
8
|
+
class RouteParserPlugin {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
appPath: options.appPath || './client/src/app.tsx',
|
|
12
|
+
outputPath: options.outputPath || './dist/client/routes.json',
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
// 缓存相关属性 - 直接存储在实例上
|
|
16
|
+
this.lastAppPathHash = null;
|
|
17
|
+
this.cachedRoutes = null;
|
|
18
|
+
}
|
|
19
|
+
// 统一的日志函数
|
|
20
|
+
log(level, message, ...args) {
|
|
21
|
+
const prefix = '[route-parser]';
|
|
22
|
+
const logMessage = `${prefix} ${message}`;
|
|
23
|
+
switch (level) {
|
|
24
|
+
case 'log':
|
|
25
|
+
console.log(logMessage, ...args);
|
|
26
|
+
break;
|
|
27
|
+
case 'warn':
|
|
28
|
+
console.warn(logMessage, ...args);
|
|
29
|
+
break;
|
|
30
|
+
case 'error':
|
|
31
|
+
console.error(logMessage, ...args);
|
|
32
|
+
break;
|
|
33
|
+
case 'info':
|
|
34
|
+
console.info(logMessage, ...args);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
console.log(logMessage, ...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
apply(compiler) {
|
|
41
|
+
const pluginName = 'RouteParserPlugin';
|
|
42
|
+
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
|
|
43
|
+
try {
|
|
44
|
+
// 检查是否需要重新生成路由
|
|
45
|
+
if (this.shouldRegenerateRoutes()) {
|
|
46
|
+
const routes = this.parseRoutes();
|
|
47
|
+
this.cachedRoutes = routes;
|
|
48
|
+
}
|
|
49
|
+
const routesJson = JSON.stringify(this.cachedRoutes, null, 2);
|
|
50
|
+
// 将 routes.json 添加到编译输出中
|
|
51
|
+
compilation.assets['routes.json'] = {
|
|
52
|
+
source: () => routesJson,
|
|
53
|
+
size: () => routesJson.length
|
|
54
|
+
};
|
|
55
|
+
callback();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.log('warn', '⚠️ 路由解析失败,使用默认路由:', error.message);
|
|
59
|
+
// 解析失败时使用默认路由
|
|
60
|
+
const defaultRoutes = [{ path: '/' }];
|
|
61
|
+
const routesJson = JSON.stringify(defaultRoutes, null, 2);
|
|
62
|
+
compilation.assets['routes.json'] = {
|
|
63
|
+
source: () => routesJson,
|
|
64
|
+
size: () => routesJson.length
|
|
65
|
+
};
|
|
66
|
+
callback();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
shouldRegenerateRoutes() {
|
|
71
|
+
try {
|
|
72
|
+
const appFilePath = path.resolve(process.cwd(), this.options.appPath);
|
|
73
|
+
if (!fs.existsSync(appFilePath)) {
|
|
74
|
+
this.log('warn', `⚠️ App.tsx 文件不存在: ${appFilePath}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// 计算当前文件的哈希值
|
|
78
|
+
const currentHash = this.calculateFileHash(appFilePath);
|
|
79
|
+
// 检查内存中的缓存
|
|
80
|
+
if (this.lastAppPathHash === currentHash && this.cachedRoutes) {
|
|
81
|
+
return false; // 不需要重新生成
|
|
82
|
+
}
|
|
83
|
+
this.lastAppPathHash = currentHash;
|
|
84
|
+
return true; // 需要重新生成
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.log('warn', '⚠️ 检查文件变更时出错:', error.message);
|
|
88
|
+
return true; // 出错时重新生成
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
calculateFileHash(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
94
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.log('warn', '⚠️ 计算文件哈希失败:', error.message);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parseRoutes() {
|
|
102
|
+
try {
|
|
103
|
+
const appFilePath = path.resolve(process.cwd(), this.options.appPath);
|
|
104
|
+
if (!fs.existsSync(appFilePath)) {
|
|
105
|
+
throw new Error(`App.tsx 文件不存在: ${appFilePath}`);
|
|
106
|
+
}
|
|
107
|
+
const sourceCode = fs.readFileSync(appFilePath, 'utf-8');
|
|
108
|
+
// 解析 TypeScript/JSX 代码
|
|
109
|
+
const ast = parse(sourceCode, {
|
|
110
|
+
sourceType: 'module',
|
|
111
|
+
plugins: [
|
|
112
|
+
'jsx',
|
|
113
|
+
'typescript',
|
|
114
|
+
'decorators-legacy',
|
|
115
|
+
'classProperties',
|
|
116
|
+
'objectRestSpread',
|
|
117
|
+
'functionBind',
|
|
118
|
+
'exportDefaultFrom',
|
|
119
|
+
'exportNamespaceFrom',
|
|
120
|
+
'dynamicImport',
|
|
121
|
+
'nullishCoalescingOperator',
|
|
122
|
+
'optionalChaining'
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
// 使用 Set 来存储路径,自动去重
|
|
126
|
+
const routeSet = new Set();
|
|
127
|
+
// 用于跟踪路由嵌套
|
|
128
|
+
const routeStack = [];
|
|
129
|
+
const self = this;
|
|
130
|
+
traverse(ast, {
|
|
131
|
+
JSXElement: {
|
|
132
|
+
enter(path) {
|
|
133
|
+
const { openingElement } = path.node;
|
|
134
|
+
// 检查是否是 Route 组件
|
|
135
|
+
if (self.isRouteComponent(openingElement)) {
|
|
136
|
+
const routeInfo = self.extractRouteInfo(openingElement);
|
|
137
|
+
// 将当前路由信息推入堆栈
|
|
138
|
+
routeStack.push(routeInfo);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
exit(path) {
|
|
142
|
+
const { openingElement } = path.node;
|
|
143
|
+
// 当离开 Route 元素时,从堆栈中弹出
|
|
144
|
+
if (self.isRouteComponent(openingElement)) {
|
|
145
|
+
const currentRoute = routeStack.pop();
|
|
146
|
+
// 跳过通配符路径
|
|
147
|
+
if (currentRoute && currentRoute.path === '*') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// 如果有路径或是索引路由
|
|
151
|
+
if (currentRoute && (currentRoute.path || currentRoute.index)) {
|
|
152
|
+
const fullPath = self.buildFullPath(routeStack, currentRoute);
|
|
153
|
+
if (fullPath) {
|
|
154
|
+
routeSet.add(fullPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// 将 Set 转换为数组返回
|
|
162
|
+
const routes = Array.from(routeSet).map(routePath => ({ path: routePath }));
|
|
163
|
+
return routes.length > 0 ? routes : [{ path: '/' }];
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
// 如果解析失败,返回默认路由
|
|
167
|
+
this.log('warn', '⚠️ 路由解析失败,使用默认路由:', error.message);
|
|
168
|
+
return [{ path: '/' }];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
isRouteComponent(openingElement) {
|
|
172
|
+
return (t.isJSXIdentifier(openingElement.name) &&
|
|
173
|
+
openingElement.name.name === 'Route');
|
|
174
|
+
}
|
|
175
|
+
isRoutesComponent(path) {
|
|
176
|
+
const openingElement = path.node.openingElement;
|
|
177
|
+
return (t.isJSXIdentifier(openingElement.name) &&
|
|
178
|
+
openingElement.name.name === 'Routes');
|
|
179
|
+
}
|
|
180
|
+
extractRouteInfo(openingElement) {
|
|
181
|
+
const routeInfo = {};
|
|
182
|
+
// 提取所有属性
|
|
183
|
+
openingElement.attributes.forEach((attr) => {
|
|
184
|
+
if (t.isJSXAttribute(attr)) {
|
|
185
|
+
const { name } = attr.name;
|
|
186
|
+
let value;
|
|
187
|
+
// 处理不同类型的属性值
|
|
188
|
+
if (attr.value) {
|
|
189
|
+
if (t.isStringLiteral(attr.value)) {
|
|
190
|
+
value = attr.value.value;
|
|
191
|
+
}
|
|
192
|
+
else if (t.isJSXExpressionContainer(attr.value)) {
|
|
193
|
+
const expression = attr.value.expression;
|
|
194
|
+
if (t.isStringLiteral(expression)) {
|
|
195
|
+
value = expression.value;
|
|
196
|
+
}
|
|
197
|
+
else if (t.isTemplateLiteral(expression)) {
|
|
198
|
+
// 处理模板字符串
|
|
199
|
+
value = this.evaluateTemplateLiteral(expression);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 对于其他表达式,设为 true
|
|
203
|
+
value = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// 对于没有值的属性(如 index),设为 true
|
|
209
|
+
value = true;
|
|
210
|
+
}
|
|
211
|
+
routeInfo[name] = value;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return routeInfo;
|
|
215
|
+
}
|
|
216
|
+
getJSXAttribute(element, name) {
|
|
217
|
+
return element.attributes.find(attr => t.isJSXAttribute(attr) &&
|
|
218
|
+
t.isJSXIdentifier(attr.name) &&
|
|
219
|
+
attr.name.name === name);
|
|
220
|
+
}
|
|
221
|
+
buildFullPath(routeStack, currentRoute) {
|
|
222
|
+
// 构建完整路径
|
|
223
|
+
let fullPath = '';
|
|
224
|
+
// 遍历堆栈中的所有父路由
|
|
225
|
+
for (let i = 0; i < routeStack.length; i++) {
|
|
226
|
+
if (routeStack[i].path) {
|
|
227
|
+
// 确保路径格式正确(开头有/,结尾没有/)
|
|
228
|
+
let parentPath = routeStack[i].path;
|
|
229
|
+
if (!parentPath.startsWith('/'))
|
|
230
|
+
parentPath = `/${parentPath}`;
|
|
231
|
+
if (parentPath.endsWith('/') && parentPath !== '/') {
|
|
232
|
+
parentPath = parentPath.slice(0, -1);
|
|
233
|
+
}
|
|
234
|
+
fullPath += parentPath === '/' ? '' : parentPath;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 添加当前路由的路径
|
|
238
|
+
if (currentRoute.index) {
|
|
239
|
+
// 索引路由使用父路由的路径
|
|
240
|
+
return fullPath || '/';
|
|
241
|
+
}
|
|
242
|
+
else if (currentRoute.path) {
|
|
243
|
+
const routePath = currentRoute.path;
|
|
244
|
+
// 跳过通配符路径
|
|
245
|
+
if (routePath === '*') {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
// 处理相对路径(不以/开头的路径)
|
|
249
|
+
if (!routePath.startsWith('/')) {
|
|
250
|
+
fullPath = `${fullPath}/${routePath}`;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
fullPath = routePath; // 绝对路径覆盖父路径
|
|
254
|
+
}
|
|
255
|
+
// 确保路径格式正确
|
|
256
|
+
if (fullPath === '')
|
|
257
|
+
fullPath = '/';
|
|
258
|
+
if (!fullPath.startsWith('/'))
|
|
259
|
+
fullPath = `/${fullPath}`;
|
|
260
|
+
return fullPath;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
evaluateTemplateLiteral(templateLiteral) {
|
|
265
|
+
// 简单处理模板字符串,这里主要处理 `*` 这种情况
|
|
266
|
+
const quasis = templateLiteral.quasis;
|
|
267
|
+
const expressions = templateLiteral.expressions;
|
|
268
|
+
if (quasis.length === 1 && expressions.length === 0) {
|
|
269
|
+
return quasis[0].value.raw;
|
|
270
|
+
}
|
|
271
|
+
// 对于复杂的模板字符串,返回原始字符串
|
|
272
|
+
return quasis.map(q => q.value.raw).join('');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
module.exports = RouteParserPlugin;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 加载性能监控脚本
|
|
4
|
+
* @param {object} option
|
|
5
|
+
* @param {string} [option.bid='apaas_ai']
|
|
6
|
+
* @param {string} [option.globalName='KSlardarWeb']
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
function getSlardarScript(option = {}) {
|
|
10
|
+
const bid = option.bid || 'apaas_ai';
|
|
11
|
+
const globalName = option.globalName || 'KSlardarWeb';
|
|
12
|
+
return `
|
|
13
|
+
<script>
|
|
14
|
+
// 创建 Slardar 脚本元素
|
|
15
|
+
const slardarScript = document.createElement('script');
|
|
16
|
+
slardarScript.src = 'https://lf3-short.ibytedapm.com/slardar/fe/sdk-web/browser.cn.js?bid=${bid}&globalName=${globalName}';
|
|
17
|
+
slardarScript.crossOrigin = 'anonymous';
|
|
18
|
+
|
|
19
|
+
// 添加 onload 事件处理
|
|
20
|
+
slardarScript.onload = function() {
|
|
21
|
+
// 脚本加载完成后执行初始化
|
|
22
|
+
if (window.KSlardarWeb) {
|
|
23
|
+
window.KSlardarWeb('init', {
|
|
24
|
+
bid: 'apaas_ai',
|
|
25
|
+
// 四种类型:dev/boe/pre/online
|
|
26
|
+
env: 'online',
|
|
27
|
+
});
|
|
28
|
+
window.KSlardarWeb('start');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 添加错误处理
|
|
33
|
+
slardarScript.onerror = function() {
|
|
34
|
+
console.warn('Failed to load Slardar script');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 将脚本添加到页面
|
|
38
|
+
document.head.appendChild(slardarScript);
|
|
39
|
+
|
|
40
|
+
// 添加 TTI 监控脚本
|
|
41
|
+
const performanceScript = document.createElement('script');
|
|
42
|
+
performanceScript.src = 'https://sf3-scmcdn-cn.feishucdn.com/obj/unpkg/byted/performance/0.1.0/dist/performance.iife.js';
|
|
43
|
+
document.head.appendChild(performanceScript);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
class SlardarPerformanceMonitorPlugin {
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.options = options || {};
|
|
51
|
+
}
|
|
52
|
+
apply(compiler) {
|
|
53
|
+
compiler.hooks.compilation.tap('SlardarPerformanceMonitorPlugin', compilation => {
|
|
54
|
+
try {
|
|
55
|
+
// 尝试获取HtmlRspackPlugin的hooks
|
|
56
|
+
let HtmlPlugin;
|
|
57
|
+
try {
|
|
58
|
+
HtmlPlugin = require('@rspack/core').rspack.HtmlRspackPlugin;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
// 如果html-rspack-plugin不可用,尝试使用html-webpack-plugin
|
|
62
|
+
try {
|
|
63
|
+
HtmlPlugin = require('html-webpack-plugin');
|
|
64
|
+
}
|
|
65
|
+
catch (e2) {
|
|
66
|
+
console.warn('SlardarPerformanceMonitorPlugin: html-rspack-plugin or html-webpack-plugin not found');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const hooks = HtmlPlugin.getHooks(compilation);
|
|
71
|
+
// 在生成 HTML 前修改内容
|
|
72
|
+
hooks.beforeEmit.tapAsync('SlardarPerformanceMonitorPlugin', (data, cb) => {
|
|
73
|
+
const position = this.options.position ?? 'head-end';
|
|
74
|
+
const snippet = this.options.snippet ?? getSlardarScript();
|
|
75
|
+
if (!snippet)
|
|
76
|
+
return;
|
|
77
|
+
if (position === 'body-end') {
|
|
78
|
+
data.html = data.html.replace('</body>', `${snippet}\n</body>`);
|
|
79
|
+
}
|
|
80
|
+
else if (position === 'body-start') {
|
|
81
|
+
data.html = data.html.replace('<body>', `<body>\n${snippet}`);
|
|
82
|
+
}
|
|
83
|
+
else if (position === 'head-end') {
|
|
84
|
+
data.html = data.html.replace('</head>', `${snippet}\n</head>`);
|
|
85
|
+
}
|
|
86
|
+
cb(null, data);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error('Error in SlardarPerformanceMonitorPlugin:', error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 导出插件
|
|
96
|
+
module.exports = SlardarPerformanceMonitorPlugin;
|
package/lib/rspack.config.js
CHANGED
|
@@ -8,13 +8,14 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const core_1 = __importDefault(require("@rspack/core"));
|
|
9
9
|
const rspack_1 = require("./recommend/rspack");
|
|
10
10
|
const webpack_merge_1 = __importDefault(require("webpack-merge"));
|
|
11
|
+
// 妙搭应用使用的rspack配置
|
|
11
12
|
// eslint-disable-next-line max-lines-per-function
|
|
12
13
|
function createRspackConfig(options) {
|
|
13
|
-
const {
|
|
14
|
-
console.log('
|
|
15
|
-
console.log('
|
|
14
|
+
const { enableReactRefresh = false, isDevBuildMode = true } = options;
|
|
15
|
+
console.log('isDevBuildMode:', isDevBuildMode);
|
|
16
|
+
console.log('enableReactRefresh:', enableReactRefresh);
|
|
16
17
|
// 构建外部依赖配置,对应vite的rollupOptions.external和globals
|
|
17
|
-
const externals =
|
|
18
|
+
const externals = isDevBuildMode
|
|
18
19
|
? {
|
|
19
20
|
antd: 'antd',
|
|
20
21
|
'@ant-design/icons': 'icons',
|
|
@@ -25,11 +26,13 @@ function createRspackConfig(options) {
|
|
|
25
26
|
}
|
|
26
27
|
: {};
|
|
27
28
|
const recommendConfig = (0, rspack_1.createRecommendRspackConfig)({
|
|
28
|
-
|
|
29
|
-
isDev,
|
|
29
|
+
enableReactRefresh,
|
|
30
|
+
isDev: isDevBuildMode,
|
|
31
|
+
needRoutes: false,
|
|
32
|
+
runtimeMode: '',
|
|
30
33
|
});
|
|
31
34
|
return (0, webpack_merge_1.default)(recommendConfig, {
|
|
32
|
-
mode:
|
|
35
|
+
mode: isDevBuildMode ? 'development' : 'production',
|
|
33
36
|
entry: './src/index.tsx',
|
|
34
37
|
output: {
|
|
35
38
|
publicPath: '/',
|
|
@@ -60,7 +63,7 @@ function createRspackConfig(options) {
|
|
|
60
63
|
},
|
|
61
64
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
|
62
65
|
},
|
|
63
|
-
devServer:
|
|
66
|
+
devServer: isDevBuildMode
|
|
64
67
|
? undefined
|
|
65
68
|
: {
|
|
66
69
|
host: '::',
|
|
@@ -90,5 +93,46 @@ function createRspackConfig(options) {
|
|
|
90
93
|
},
|
|
91
94
|
}),
|
|
92
95
|
],
|
|
96
|
+
optimization: {
|
|
97
|
+
moduleIds: 'deterministic',
|
|
98
|
+
concatenateModules: true,
|
|
99
|
+
minimize: true, // 对应vite的minify配置
|
|
100
|
+
minimizer: [
|
|
101
|
+
new core_1.default.SwcJsMinimizerRspackPlugin({
|
|
102
|
+
minimizerOptions: {
|
|
103
|
+
// 保持不压缩
|
|
104
|
+
minify: !isDevBuildMode,
|
|
105
|
+
mangle: !isDevBuildMode,
|
|
106
|
+
format: {
|
|
107
|
+
beautify: isDevBuildMode,
|
|
108
|
+
comments: false,
|
|
109
|
+
},
|
|
110
|
+
compress: {
|
|
111
|
+
keep_classnames: true,
|
|
112
|
+
keep_fnames: true,
|
|
113
|
+
keep_fargs: !isDevBuildMode,
|
|
114
|
+
unused: true,
|
|
115
|
+
dead_code: true,
|
|
116
|
+
drop_debugger: true,
|
|
117
|
+
// FIXME: 先临时开始 console.log
|
|
118
|
+
// drop_console: !isDevBuildMode,
|
|
119
|
+
const_to_let: !isDevBuildMode,
|
|
120
|
+
booleans_as_integers: !isDevBuildMode,
|
|
121
|
+
booleans: !isDevBuildMode,
|
|
122
|
+
// maybe unsafe
|
|
123
|
+
reduce_funcs: !isDevBuildMode,
|
|
124
|
+
reduce_vars: !isDevBuildMode,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
new core_1.default.LightningCssMinimizerRspackPlugin(),
|
|
129
|
+
],
|
|
130
|
+
sideEffects: true,
|
|
131
|
+
usedExports: true,
|
|
132
|
+
innerGraph: true,
|
|
133
|
+
providedExports: true,
|
|
134
|
+
mergeDuplicateChunks: true,
|
|
135
|
+
splitChunks: false, // 禁用代码分割,保持单文件输出
|
|
136
|
+
},
|
|
93
137
|
});
|
|
94
138
|
}
|
package/lib/tailwind.config.js
CHANGED
|
@@ -6,10 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createTailwindConfig = createTailwindConfig;
|
|
7
7
|
const webpack_merge_1 = __importDefault(require("webpack-merge"));
|
|
8
8
|
const tailwind_1 = require("./recommend/tailwind");
|
|
9
|
+
/** 妙搭使用的tailwind配置 */
|
|
9
10
|
function createTailwindConfig(options) {
|
|
10
11
|
const { isDevBuildMode = true } = options;
|
|
11
12
|
const recommendConfig = (0, tailwind_1.createRecommendTailwindConfig)({
|
|
12
13
|
isDev: isDevBuildMode,
|
|
14
|
+
// 妙搭需要扫描内网包
|
|
15
|
+
packageName: '@byted/spark-framework',
|
|
13
16
|
});
|
|
14
17
|
return (0, webpack_merge_1.default)(recommendConfig, {
|
|
15
18
|
content: ['./src/**/*.{ts,tsx}'],
|
package/package.json
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-presets",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.overlay.1",
|
|
4
4
|
"files": [
|
|
5
5
|
"lib"
|
|
6
6
|
],
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && npm run copy:json",
|
|
12
|
+
"copy:json": "cp -r src/*.json lib",
|
|
13
|
+
"watch": "tsc --watch",
|
|
14
|
+
"bump": "changeset version",
|
|
15
|
+
"change": "changeset",
|
|
16
|
+
"check": "biome check --write",
|
|
17
|
+
"dev": "rslib build --watch",
|
|
18
|
+
"format": "biome format --write",
|
|
19
|
+
"storybook": "storybook dev",
|
|
20
|
+
"test": "echo 0",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
10
23
|
"dependencies": {
|
|
11
24
|
"@babel/core": "^7.28.0",
|
|
12
25
|
"@babel/parser": "^7.28.0",
|
|
13
26
|
"@babel/traverse": "^7.28.0",
|
|
14
27
|
"@babel/types": "^7.28.2",
|
|
15
28
|
"@eslint/js": "^9.34.0",
|
|
29
|
+
"@lark-apaas/miaoda-inspector-babel-plugin": "^1.0.0",
|
|
30
|
+
"@lark-apaas/miaoda-inspector-jsx-runtime": "^1.0.0",
|
|
16
31
|
"@react-dev-inspector/middleware": "^2.0.1",
|
|
17
32
|
"@rsdoctor/rspack-plugin": "^1.1.8",
|
|
18
33
|
"@rspack/dev-server": "^1.1.3",
|
|
@@ -25,14 +40,16 @@
|
|
|
25
40
|
"eslint-plugin-react-refresh": "^0.4.20",
|
|
26
41
|
"globals": "^15.15.0",
|
|
27
42
|
"magic-string": "^0.30.18",
|
|
43
|
+
"postcss-import": "^16.1.1",
|
|
44
|
+
"tailwindcss": "^4.1.13",
|
|
28
45
|
"tsconfig-paths-webpack-plugin": "^4.2.0",
|
|
29
46
|
"typescript-eslint": "^8.41.0",
|
|
30
|
-
"webpack-merge": "^6.0.1"
|
|
31
|
-
"tailwindcss": "^4.1.13",
|
|
32
|
-
"@lark-apaas/miaoda-inspector-babel-plugin": "0.1.0-alpha.1",
|
|
33
|
-
"@lark-apaas/miaoda-inspector-jsx-runtime": "0.1.0-alpha.2"
|
|
47
|
+
"webpack-merge": "^6.0.1"
|
|
34
48
|
},
|
|
35
49
|
"devDependencies": {
|
|
50
|
+
"@babel/parser": "^7.28.4",
|
|
51
|
+
"@babel/traverse": "^7.28.4",
|
|
52
|
+
"@babel/types": "^7.28.4",
|
|
36
53
|
"@biomejs/biome": "2.0.6",
|
|
37
54
|
"@changesets/cli": "^2.29.5",
|
|
38
55
|
"@rspack/core": "^1.4.4",
|
|
@@ -44,17 +61,5 @@
|
|
|
44
61
|
"peerDependencies": {
|
|
45
62
|
"react": ">=16.14.0",
|
|
46
63
|
"react-dom": ">=16.14.0"
|
|
47
|
-
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"build": "tsc && npm run copy:json",
|
|
50
|
-
"copy:json": "cp -r src/*.json lib",
|
|
51
|
-
"watch": "tsc --watch",
|
|
52
|
-
"bump": "changeset version",
|
|
53
|
-
"change": "changeset",
|
|
54
|
-
"check": "biome check --write",
|
|
55
|
-
"dev": "rslib build --watch",
|
|
56
|
-
"format": "biome format --write",
|
|
57
|
-
"storybook": "storybook dev",
|
|
58
|
-
"test": "echo 0"
|
|
59
64
|
}
|
|
60
|
-
}
|
|
65
|
+
}
|