hw-weapp-compiler 1.0.26 → 1.0.27
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 +28 -0
- package/build/config/getAppConfig.js +1 -0
- package/build/config/getEntries.js +17 -0
- package/build/plugin/index.js +140 -7
- package/build/prod.js +23 -21
- package/build/utils/createCompilerHandler.js +9 -2
- package/build/utils/independentPackages.js +31 -0
- package/build/utils/upload.js +155 -11
- package/build/webpack.config.js +71 -13
- package/index.js +0 -0
- package/package.json +2 -2
- package/test/independent-packages.js +307 -0
- package/test/plugin-upload.js +265 -0
- package/test/upload-async-client.js +1 -1
- package/test/upload.js +61 -3
- package/test/webpack-upload-config.js +115 -0
package/README.md
CHANGED
|
@@ -78,3 +78,31 @@ module.exports = {
|
|
|
78
78
|
]
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
|
+
|
|
82
|
+
## 独立分包(受限支持)
|
|
83
|
+
|
|
84
|
+
在 `src/app.json` 中配置 `independent: true`,`subpackages` 和 `subPackages` 两种写法均可:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"pages": ["pages/index/index"],
|
|
89
|
+
"subpackages": [{
|
|
90
|
+
"root": "activity",
|
|
91
|
+
"pages": ["pages/index/index"],
|
|
92
|
+
"independent": true
|
|
93
|
+
}]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
主包和普通分包沿用原有编译流程;每个独立分包单独编译,生成包内的 runtime、公共 JS/WXSS 和资源。
|
|
98
|
+
页面及组件入口会自动加载所需的包内公共文件。没有独立分包时仍使用单份 webpack 配置。
|
|
99
|
+
|
|
100
|
+
- 独立分包的原生组件、JSON、WXML、WXS、样式和本地资源必须位于自己的 `root` 内;跨包原生引用会在构建时报错。
|
|
101
|
+
- `usingComponents` 需显式写在独立包的页面或组件 JSON 中,使用包内相对路径或 `/activity/...` 路径。不自动搬迁主包组件、npm 原生组件或插件组件。
|
|
102
|
+
- 可以静态引用包外的普通 JS 模块和纯 JS npm 依赖,编译器会将它们编入该独立包。依赖若继续引用包外样式、JSON 等原生文件,也会报错。同一包内共享模块实例,不同包之间不共享模块缓存。
|
|
103
|
+
- 本地资源自动输出到 `<root>/assets/`,引用使用小程序根路径;配置 CDN `publicPath` 时沿用 CDN。动态拼接的资源路径不能自动收集,需通过 `copyFiles` 将包内资源复制到同一包内,例如 `{ from: 'activity/images', to: 'activity/images' }`。
|
|
104
|
+
- 业务代码不能假设主包的 `App` 已初始化,也不能依赖 `app.wxss` 的样式。本版不支持独立包内产生异步 chunk 的动态 `import()` 或分包异步化。
|
|
105
|
+
- 修改已有 JS、模板、样式或资源可以增量构建;修改分包配置、增删页面或组件、改变组件依赖关系后,需要重启 `dev`。
|
|
106
|
+
|
|
107
|
+
分包的 `root` 不能重复或互相嵌套。各包会重复编入自身使用的共享 JS/npm 依赖,因此可能增加总产物体积和编译耗时。
|
|
108
|
+
多包构建只在启动时统一清理 `dist`;`-a` 分析模式会生成各包的 `bundle-report.html` 静态报告。
|
|
@@ -4,8 +4,11 @@ const { addNodeModulesUsingComponent } = require('../utils/isNodeModulesUsingCom
|
|
|
4
4
|
const { compatiblePath } = require('../utils/compatiblePath');
|
|
5
5
|
const { getAppConfig } = require('./getAppConfig');
|
|
6
6
|
const { PATTERNS, SRC_DIR } = require('./constants');
|
|
7
|
+
const { getIndependentRoots, isWithinRoot } = require('../utils/independentPackages');
|
|
7
8
|
|
|
8
9
|
const appConfig = getAppConfig();
|
|
10
|
+
const independentRoots = getIndependentRoots(appConfig);
|
|
11
|
+
const ownerOf = (file) => independentRoots.find((root) => isWithinRoot(path.relative(SRC_DIR, file), root));
|
|
9
12
|
|
|
10
13
|
// 惰性异步初始化:entry / usingComponents 收集改为 Promise.all 并发读 json,
|
|
11
14
|
// 避免在 webpack 启动前对深组件树做同步串行 readJSONSync/existsSync/require.resolve。
|
|
@@ -22,6 +25,9 @@ function buildBaseEntries() {
|
|
|
22
25
|
|
|
23
26
|
// pages
|
|
24
27
|
(appConfig.pages || []).forEach((page) => {
|
|
28
|
+
if (ownerOf(path.resolve(SRC_DIR, page))) {
|
|
29
|
+
throw new Error(`主包 pages 不能包含独立分包页面:${page}`);
|
|
30
|
+
}
|
|
25
31
|
addEntry(page, path.resolve(SRC_DIR, page));
|
|
26
32
|
});
|
|
27
33
|
|
|
@@ -33,6 +39,9 @@ function buildBaseEntries() {
|
|
|
33
39
|
// subpackages
|
|
34
40
|
(appConfig.subpackages || []).forEach((pkg) => {
|
|
35
41
|
(pkg.pages || []).forEach((page) => {
|
|
42
|
+
if (pkg.independent && !isWithinRoot(compatiblePath(path.join(pkg.root, page)), compatiblePath(pkg.root).replace(/\/$/, ''))) {
|
|
43
|
+
throw new Error(`独立分包页面越界:${pkg.root}/${page}`);
|
|
44
|
+
}
|
|
36
45
|
addEntry(path.join(pkg.root, page), path.resolve(SRC_DIR, pkg.root, page));
|
|
37
46
|
});
|
|
38
47
|
});
|
|
@@ -102,10 +111,18 @@ async function collectUsingComponents(entryFiles, entries) {
|
|
|
102
111
|
|
|
103
112
|
// 同步解析路径(existsSync/require.resolve 不宜并发但单 round 内串行,正是被批处理掉的是 readJSON)
|
|
104
113
|
newComponents.forEach(({ key, value, dir }) => {
|
|
114
|
+
const owner = ownerOf(dir);
|
|
115
|
+
if (owner && PATTERNS.PLUGIN_PATH.test(value)) {
|
|
116
|
+
throw new Error(`独立分包 ${owner} 仅支持包内原生组件:${value}`);
|
|
117
|
+
}
|
|
105
118
|
const resolved = resolveUsingComponentPath(key, value, dir);
|
|
106
119
|
if (!resolved) {
|
|
107
120
|
return;
|
|
108
121
|
}
|
|
122
|
+
const targetOwner = ownerOf(resolved);
|
|
123
|
+
if (owner !== targetOwner && (owner || targetOwner)) {
|
|
124
|
+
throw new Error(`独立分包组件引用越界:${dir} -> ${value}。请将原生组件及其依赖放在同一独立包内。`);
|
|
125
|
+
}
|
|
109
126
|
|
|
110
127
|
if (fse.existsSync(`${resolved}.json`)) {
|
|
111
128
|
jsonQueue.push(`${resolved}.json`);
|
package/build/plugin/index.js
CHANGED
|
@@ -9,7 +9,8 @@ const { RawSource } = require('webpack-sources');
|
|
|
9
9
|
|
|
10
10
|
const { addToUploadQueue } = require('../utils/upload');
|
|
11
11
|
const { compatiblePath } = require('../utils/compatiblePath');
|
|
12
|
-
const {
|
|
12
|
+
const { isWithinRoot } = require('../utils/independentPackages');
|
|
13
|
+
const { PATTERNS, ASSET_EXT_PATTERN, ASSETS_DIR, SRC_DIR } = require('../config/constants');
|
|
13
14
|
|
|
14
15
|
const pluginName = 'WeappCompilerPlugin';
|
|
15
16
|
|
|
@@ -161,17 +162,148 @@ function injectSubpackageRequires(content, assetName, subpackages) {
|
|
|
161
162
|
return result;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// 独立包没有 app.js/app.wxss,每个页面和组件都必须加载自己的入口依赖。
|
|
166
|
+
function injectIndependentEntrypoints(compilation) {
|
|
167
|
+
compilation.entrypoints.forEach((entrypoint, name) => {
|
|
168
|
+
const jsName = `${name}.js`;
|
|
169
|
+
const wxssName = `${name}.wxss`;
|
|
170
|
+
const files = [...new Set(entrypoint.getFiles())];
|
|
171
|
+
const js = compilation.getAsset(jsName);
|
|
172
|
+
if (js) {
|
|
173
|
+
const requires = files.filter((file) => file !== jsName && PATTERNS.JS_EXT.test(file))
|
|
174
|
+
.map((file) => `require(${JSON.stringify(getRelativeModulePath(path.dirname(jsName), file))});\n`);
|
|
175
|
+
compilation.updateAsset(jsName, new RawSource(requires.join('') + js.source.source().toString()));
|
|
176
|
+
}
|
|
177
|
+
const imports = files.filter((file) => file !== wxssName && PATTERNS.WXSS_EXT.test(file))
|
|
178
|
+
.map((file) => `@import ${JSON.stringify(getRelativeModulePath(path.dirname(wxssName), file))};\n`);
|
|
179
|
+
if (imports.length) {
|
|
180
|
+
const wxss = compilation.getAsset(wxssName);
|
|
181
|
+
const source = new RawSource(imports.join('') + (wxss ? wxss.source.source().toString() : ''));
|
|
182
|
+
if (wxss) compilation.updateAsset(wxssName, source);
|
|
183
|
+
else compilation.emitAsset(wxssName, source);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validatePackageBoundary(compilation, independentRoot, independentRoots) {
|
|
189
|
+
const errors = new Set();
|
|
190
|
+
const checkFile = (file, native) => {
|
|
191
|
+
if (!file) return;
|
|
192
|
+
const relative = compatiblePath(path.relative(SRC_DIR, file.split('?')[0]));
|
|
193
|
+
if (independentRoot) {
|
|
194
|
+
if (native && !isWithinRoot(relative, independentRoot)) {
|
|
195
|
+
errors.add(`独立分包 ${independentRoot} 原生文件引用越界:${relative}。请将组件、模板、样式和本地资源放在包内。`);
|
|
196
|
+
}
|
|
197
|
+
} else if (independentRoots.some((root) => isWithinRoot(relative, root))) {
|
|
198
|
+
errors.add(`主包或普通分包不能引用独立分包源文件:${relative}`);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const checkNativeDependencies = (files) => {
|
|
202
|
+
for (const file of files || []) {
|
|
203
|
+
if (/\.(wxml|wxs|wxss|css|less)$/i.test(file) || ASSET_EXT_PATTERN.test(file)) checkFile(file, true);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
for (const module of compilation.modules) {
|
|
207
|
+
// 普通 JS 可以从共享源码/npm 编入包内;原生文件不能自动搬迁。
|
|
208
|
+
checkFile(module.resource, module.resource && !/\.[cm]?js(?:\?|$)/i.test(module.resource));
|
|
209
|
+
}
|
|
210
|
+
// less 的 @import 等依赖不一定是 webpack module;不检查 package.json 等构建元数据。
|
|
211
|
+
checkNativeDependencies(compilation.fileDependencies);
|
|
212
|
+
if (independentRoot) {
|
|
213
|
+
for (const chunk of compilation.chunks) {
|
|
214
|
+
if (!chunk.canBeInitial()) errors.add(`独立分包 ${independentRoot} 暂不支持动态 import 或其他异步 chunk。请使用静态依赖。`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
compilation.getAssets().forEach(({ name, source }) => {
|
|
218
|
+
const owner = independentRoots.find((root) => isWithinRoot(name, root)) || '';
|
|
219
|
+
if (owner !== independentRoot) errors.add(`分包产物越界:${independentRoot || '主包'} -> ${name}`);
|
|
220
|
+
if (!independentRoot || !PATTERNS.JSON_EXT.test(name)) return;
|
|
221
|
+
let json;
|
|
222
|
+
try { json = JSON.parse(source.source().toString()); } catch (_) { return; }
|
|
223
|
+
Object.values(json.usingComponents || {}).forEach((request) => {
|
|
224
|
+
const target = path.posix.normalize(request.startsWith('/')
|
|
225
|
+
? request.slice(1) : path.posix.join(path.posix.dirname(compatiblePath(name)), request));
|
|
226
|
+
if (!isWithinRoot(target, independentRoot) || !compilation.getAsset(`${target}.js`)) {
|
|
227
|
+
errors.add(`独立分包组件引用无效:${name} -> ${request}。仅支持包内组件,新增组件后请重启 dev。`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
errors.forEach((message) => compilation.errors.push(new Error(message)));
|
|
232
|
+
}
|
|
233
|
+
|
|
164
234
|
class WeappPlugin {
|
|
165
|
-
constructor(
|
|
235
|
+
constructor({
|
|
236
|
+
quiet = false,
|
|
237
|
+
waitForUpload = false,
|
|
238
|
+
independentRoot = '',
|
|
239
|
+
independentRoots = [],
|
|
240
|
+
uploadBarrier = null,
|
|
241
|
+
} = {}) {
|
|
242
|
+
this.quiet = quiet;
|
|
243
|
+
this.waitForUpload = waitForUpload;
|
|
244
|
+
this.independentRoot = independentRoot;
|
|
245
|
+
this.independentRoots = independentRoots;
|
|
246
|
+
this.uploadBarrier = uploadBarrier;
|
|
247
|
+
}
|
|
166
248
|
|
|
167
249
|
apply(compiler) {
|
|
168
250
|
let obsAssets = [];
|
|
169
251
|
|
|
170
|
-
|
|
171
|
-
|
|
252
|
+
const enqueueAssets = (stats) => {
|
|
253
|
+
const assets = [...obsAssets];
|
|
172
254
|
obsAssets = [];
|
|
173
|
-
});
|
|
174
255
|
|
|
256
|
+
// production MultiCompiler 共享同一个屏障:所有 child 都成功后才统一上传。
|
|
257
|
+
// fatal error 不会触发 done,remaining 也就不会归零,其他 child 不会误上传。
|
|
258
|
+
if (this.uploadBarrier) {
|
|
259
|
+
const barrier = this.uploadBarrier;
|
|
260
|
+
const hasErrors = !!(stats && stats.hasErrors());
|
|
261
|
+
barrier.remaining -= 1;
|
|
262
|
+
barrier.failed = barrier.failed || hasErrors;
|
|
263
|
+
if (!hasErrors) barrier.assets.push(...assets);
|
|
264
|
+
|
|
265
|
+
if (barrier.remaining !== 0 || barrier.failed) {
|
|
266
|
+
return Promise.resolve();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const completedAssets = [...barrier.assets];
|
|
270
|
+
barrier.assets = [];
|
|
271
|
+
return addToUploadQueue(completedAssets, {
|
|
272
|
+
quiet: this.quiet,
|
|
273
|
+
logWhenIdle: this.waitForUpload,
|
|
274
|
+
failOnError: this.waitForUpload,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 编译产物不完整时不上传资源,并让 webpack 保留原始 stats 错误详情。
|
|
279
|
+
// 若此处再抛上传错误,done hook 会只把该错误传给最终回调,遮蔽编译错误。
|
|
280
|
+
if (stats && stats.hasErrors()) {
|
|
281
|
+
return Promise.resolve();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return addToUploadQueue(assets, {
|
|
285
|
+
quiet: this.quiet,
|
|
286
|
+
logWhenIdle: this.waitForUpload,
|
|
287
|
+
failOnError: this.waitForUpload,
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (this.waitForUpload) {
|
|
292
|
+
compiler.hooks.done.tapPromise(pluginName, enqueueAssets);
|
|
293
|
+
} else {
|
|
294
|
+
compiler.hooks.done.tap(pluginName, (stats) => {
|
|
295
|
+
enqueueAssets(stats).catch((error) => {
|
|
296
|
+
console.error(error);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (this.independentRoots.length) {
|
|
302
|
+
// afterCompile 时 loader/子编译的 fileDependencies 已完整汇总,且尚未写入产物。
|
|
303
|
+
compiler.hooks.afterCompile.tap(pluginName, (compilation) => {
|
|
304
|
+
validatePackageBoundary(compilation, this.independentRoot, this.independentRoots);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
175
307
|
compiler.hooks.compilation.tap(pluginName, (compilation) => {
|
|
176
308
|
compilation.hooks.afterProcessAssets.tap(
|
|
177
309
|
{
|
|
@@ -213,14 +345,15 @@ class WeappPlugin {
|
|
|
213
345
|
}
|
|
214
346
|
|
|
215
347
|
const before = content;
|
|
216
|
-
const after = injectGlobalModules(content, assetName, flags);
|
|
217
|
-
const finalContent = injectSubpackageRequires(after, assetName, subpackages);
|
|
348
|
+
const after = injectGlobalModules(content, assetName, this.independentRoot ? {} : flags);
|
|
349
|
+
const finalContent = this.independentRoot ? after : injectSubpackageRequires(after, assetName, subpackages);
|
|
218
350
|
|
|
219
351
|
if (finalContent !== before) {
|
|
220
352
|
compilation.updateAsset(asset.name, new RawSource(finalContent));
|
|
221
353
|
}
|
|
222
354
|
}),
|
|
223
355
|
);
|
|
356
|
+
if (this.independentRoot) injectIndependentEntrypoints(compilation);
|
|
224
357
|
},
|
|
225
358
|
);
|
|
226
359
|
});
|
package/build/prod.js
CHANGED
|
@@ -4,35 +4,37 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
|
|
4
4
|
const { ENV } = require('./config/constants');
|
|
5
5
|
const webpackConfig = require('./webpack.config');
|
|
6
6
|
const { createCompilerHandler } = require('./utils/createCompilerHandler');
|
|
7
|
+
const { cleanDistForDev } = require('./utils/cleanDistForDev');
|
|
7
8
|
|
|
8
9
|
module.exports = async (opts) => {
|
|
9
10
|
// 生产环境禁用 filesystem cache,与 dev 同源:output.clean 后复用旧缓存会导致副作用产物漏生成,
|
|
10
11
|
// 注入逻辑(plugin/index.js 的 PROCESS_ASSETS_STAGE_ADDITIONS)要求每次从干净的 source 重做。
|
|
11
12
|
// production 单次构建本就是冷构建,命中收益低,移除以换确定性。
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
keep: /^(project\.config\.json|weapp\.env\.json)$/,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
optimization: {
|
|
23
|
-
minimizer: [
|
|
24
|
-
new TerserPlugin({
|
|
25
|
-
extractComments: false,
|
|
26
|
-
}),
|
|
27
|
-
new CssMinimizerPlugin({
|
|
28
|
-
test: /\.(css|wxss)(\?.*)?$/i,
|
|
29
|
-
}),
|
|
30
|
-
],
|
|
13
|
+
const config = await webpackConfig(
|
|
14
|
+
{
|
|
15
|
+
mode: ENV.PROD,
|
|
16
|
+
devtool: false,
|
|
17
|
+
output: {
|
|
18
|
+
clean: {
|
|
19
|
+
keep: /^(project\.config\.json|weapp\.env\.json)$/,
|
|
31
20
|
},
|
|
32
21
|
},
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
optimization: {
|
|
23
|
+
minimizer: [
|
|
24
|
+
new TerserPlugin({
|
|
25
|
+
extractComments: false,
|
|
26
|
+
}),
|
|
27
|
+
new CssMinimizerPlugin({
|
|
28
|
+
test: /\.(css|wxss)(\?.*)?$/i,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
opts || {},
|
|
35
34
|
);
|
|
35
|
+
// 多份配置共用 dist,只在启动前清理,禁止子编译互相删除产物。
|
|
36
|
+
if (Array.isArray(config)) await cleanDistForDev();
|
|
37
|
+
const compiler = webpack(config);
|
|
36
38
|
|
|
37
39
|
compiler.run(createCompilerHandler('build'));
|
|
38
40
|
};
|
|
@@ -5,6 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
* @param {string} label - 构建阶段标签,如 'dev' 或 'build'
|
|
6
6
|
*/
|
|
7
7
|
function createCompilerHandler(label, { failOnError = label === 'build' } = {}) {
|
|
8
|
+
let lastBuildEnd = 0;
|
|
8
9
|
const markFailed = () => {
|
|
9
10
|
if (failOnError) {
|
|
10
11
|
process.exitCode = 1;
|
|
@@ -44,10 +45,16 @@ function createCompilerHandler(label, { failOnError = label === 'build' } = {})
|
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
const children = stats.stats || [stats];
|
|
49
|
+
// MultiStats 在局部 watch 重建时还包含未重建子编译的旧 stats。
|
|
50
|
+
const rebuilt = children.filter((child) => child.endTime > lastBuildEnd);
|
|
51
|
+
const start = Math.min(...(rebuilt.length ? rebuilt : children).map((child) => child.startTime));
|
|
52
|
+
const end = Math.max(...children.map((child) => child.endTime));
|
|
53
|
+
lastBuildEnd = end;
|
|
54
|
+
console.log(chalk.green(`${label} completed in ${(end - start) / 1000} seconds`));
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
module.exports = {
|
|
52
59
|
createCompilerHandler,
|
|
53
|
-
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { compatiblePath } = require('./compatiblePath');
|
|
3
|
+
|
|
4
|
+
function isWithinRoot(file, root) {
|
|
5
|
+
const name = path.posix.normalize(compatiblePath(file));
|
|
6
|
+
return name === root || name.startsWith(`${root}/`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getIndependentRoots(appConfig) {
|
|
10
|
+
const packages = appConfig.subpackages || [];
|
|
11
|
+
const roots = packages.filter((pkg) => pkg.independent === true).map((pkg) => {
|
|
12
|
+
const root = compatiblePath(pkg.root || '').replace(/\/$/, '');
|
|
13
|
+
if (!root || path.posix.isAbsolute(root) || /^[A-Za-z]:/.test(root)
|
|
14
|
+
|| root === '.' || root.split('/').some((part) => !part || part === '..' || part === '.')) {
|
|
15
|
+
throw new Error(`独立分包 root 必须是 src 内的相对目录:${pkg.root}`);
|
|
16
|
+
}
|
|
17
|
+
return root;
|
|
18
|
+
});
|
|
19
|
+
roots.forEach((root) => {
|
|
20
|
+
const overlaps = packages.filter((pkg) => {
|
|
21
|
+
const other = compatiblePath(pkg.root || '').replace(/\/$/, '');
|
|
22
|
+
return other && (isWithinRoot(root, other) || isWithinRoot(other, root));
|
|
23
|
+
});
|
|
24
|
+
if (overlaps.length !== 1) {
|
|
25
|
+
throw new Error(`独立分包 root 重复或与其他分包目录重叠:${root}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return roots;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { getIndependentRoots, isWithinRoot };
|
package/build/utils/upload.js
CHANGED
|
@@ -29,6 +29,79 @@ let ossClient;
|
|
|
29
29
|
let progress;
|
|
30
30
|
let uploadQueue = {};
|
|
31
31
|
let uploadRunner;
|
|
32
|
+
let uploadSummary;
|
|
33
|
+
|
|
34
|
+
function getProvider() {
|
|
35
|
+
return ossConfig ? 'OSS' : 'OBS';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createUploadSummary({ quiet, failOnError }) {
|
|
39
|
+
return {
|
|
40
|
+
provider: getProvider(),
|
|
41
|
+
startedAt: Date.now(),
|
|
42
|
+
assets: new Set(),
|
|
43
|
+
localCached: new Set(),
|
|
44
|
+
pending: new Set(),
|
|
45
|
+
remoteCached: 0,
|
|
46
|
+
uploaded: 0,
|
|
47
|
+
failed: 0,
|
|
48
|
+
failedFiles: [],
|
|
49
|
+
quiet,
|
|
50
|
+
failOnError,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getUploadResult(summary) {
|
|
55
|
+
return {
|
|
56
|
+
provider: summary.provider,
|
|
57
|
+
total: summary.assets.size,
|
|
58
|
+
localCached: summary.localCached.size,
|
|
59
|
+
pending: summary.pending.size,
|
|
60
|
+
remoteCached: summary.remoteCached,
|
|
61
|
+
uploaded: summary.uploaded,
|
|
62
|
+
failed: summary.failed,
|
|
63
|
+
failedFiles: [...summary.failedFiles],
|
|
64
|
+
durationMs: Date.now() - summary.startedAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function logUploadQueue({ provider, total, localCached, pending }, quiet) {
|
|
69
|
+
if (quiet) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
`[assets] upload queue: provider=${provider}, total=${total}, ` +
|
|
75
|
+
`localCached=${localCached}, pending=${pending}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logUploadResult(result, quiet) {
|
|
80
|
+
if (quiet) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const message =
|
|
85
|
+
`[assets] upload completed: provider=${result.provider}, total=${result.total}, ` +
|
|
86
|
+
`localCached=${result.localCached}, remoteCached=${result.remoteCached}, ` +
|
|
87
|
+
`uploaded=${result.uploaded}, failed=${result.failed}, ` +
|
|
88
|
+
`duration=${(result.durationMs / 1000).toFixed(2)}s`;
|
|
89
|
+
|
|
90
|
+
if (result.failed > 0) {
|
|
91
|
+
console.error(message);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getUploadError(result) {
|
|
98
|
+
const error = new Error(
|
|
99
|
+
`Asset upload failed: provider=${result.provider}, failed=${result.failed}`,
|
|
100
|
+
);
|
|
101
|
+
error.code = 'ASSET_UPLOAD_FAILED';
|
|
102
|
+
error.failedFiles = result.failedFiles;
|
|
103
|
+
return error;
|
|
104
|
+
}
|
|
32
105
|
|
|
33
106
|
function getRemoteKey(config, file) {
|
|
34
107
|
return compatiblePath(path.join(config.dir, path.relative(DIST_DIR, file)));
|
|
@@ -323,11 +396,15 @@ function doUpload(file) {
|
|
|
323
396
|
}
|
|
324
397
|
|
|
325
398
|
function updateProgress() {
|
|
399
|
+
if (uploadSummary && uploadSummary.quiet) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
326
403
|
const completed = Object.values(uploadQueue).filter((status) => status === 'completed').length;
|
|
327
404
|
const total = Object.keys(uploadQueue).length;
|
|
328
405
|
|
|
329
406
|
if (!progress) {
|
|
330
|
-
progress = new Progress('
|
|
407
|
+
progress = new Progress('syncing assets [:bar] :current/:total', {
|
|
331
408
|
total,
|
|
332
409
|
width: 40,
|
|
333
410
|
clear: true,
|
|
@@ -348,11 +425,15 @@ async function uploadFile(file) {
|
|
|
348
425
|
try {
|
|
349
426
|
await getStat(file);
|
|
350
427
|
setStorage(file, true);
|
|
428
|
+
uploadSummary.remoteCached += 1;
|
|
351
429
|
} catch (error) {
|
|
352
430
|
try {
|
|
353
431
|
await doUpload(file);
|
|
354
432
|
setStorage(file, true);
|
|
433
|
+
uploadSummary.uploaded += 1;
|
|
355
434
|
} catch (uploadError) {
|
|
435
|
+
uploadSummary.failed += 1;
|
|
436
|
+
uploadSummary.failedFiles.push(getDisplayFile(file));
|
|
356
437
|
const detail = `file=${getDisplayFile(file)}, ${formatObsError(uploadError)}`;
|
|
357
438
|
|
|
358
439
|
if (ossConfig) {
|
|
@@ -392,13 +473,23 @@ function checkUpload() {
|
|
|
392
473
|
|
|
393
474
|
await Promise.all(files.map(uploadFile));
|
|
394
475
|
}
|
|
395
|
-
})().
|
|
476
|
+
})().then(() => {
|
|
477
|
+
const result = getUploadResult(uploadSummary);
|
|
478
|
+
logUploadResult(result, uploadSummary.quiet);
|
|
479
|
+
|
|
480
|
+
if (uploadSummary.failOnError && result.failed > 0) {
|
|
481
|
+
throw getUploadError(result);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return result;
|
|
485
|
+
}).finally(() => {
|
|
396
486
|
uploadRunner = null;
|
|
397
487
|
|
|
398
488
|
// 文件状态已经写完后再清空,避免旧实现最后一个文件把已清空的队列重新写回。
|
|
399
489
|
if (!Object.values(uploadQueue).includes('pending')) {
|
|
400
490
|
progress = null;
|
|
401
491
|
uploadQueue = {};
|
|
492
|
+
uploadSummary = null;
|
|
402
493
|
return;
|
|
403
494
|
}
|
|
404
495
|
|
|
@@ -409,17 +500,20 @@ function checkUpload() {
|
|
|
409
500
|
return uploadRunner;
|
|
410
501
|
}
|
|
411
502
|
|
|
412
|
-
function addToUploadQueue(
|
|
503
|
+
function addToUploadQueue(
|
|
504
|
+
assets,
|
|
505
|
+
{ quiet = false, logWhenIdle = false, failOnError = false } = {},
|
|
506
|
+
) {
|
|
413
507
|
if (!obsConfig && !ossConfig) {
|
|
414
508
|
console.warn('请配置obsConfig 或 ossConfig,否则无法上传文件到obs 或 oss');
|
|
415
|
-
return;
|
|
509
|
+
return Promise.resolve();
|
|
416
510
|
}
|
|
417
511
|
|
|
512
|
+
const uniqueFiles = [...new Set(assets.map((asset) => path.resolve(DIST_DIR, asset)))];
|
|
418
513
|
let cachedCount = 0;
|
|
419
514
|
let pendingCount = 0;
|
|
420
515
|
|
|
421
|
-
|
|
422
|
-
const file = path.resolve(DIST_DIR, asset);
|
|
516
|
+
uniqueFiles.forEach((file) => {
|
|
423
517
|
if (getStorage(file)) {
|
|
424
518
|
cachedCount += 1;
|
|
425
519
|
return;
|
|
@@ -430,14 +524,64 @@ function addToUploadQueue(assets) {
|
|
|
430
524
|
}
|
|
431
525
|
});
|
|
432
526
|
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
527
|
+
if (pendingCount > 0) {
|
|
528
|
+
if (!uploadSummary) {
|
|
529
|
+
uploadSummary = createUploadSummary({ quiet, failOnError });
|
|
530
|
+
} else {
|
|
531
|
+
// 同一个 watch 上传批次可能由多次编译共同追加;任一调用要求严格失败或正常日志时,
|
|
532
|
+
// 整个批次都采用更严格、可见的配置。
|
|
533
|
+
uploadSummary.failOnError = uploadSummary.failOnError || failOnError;
|
|
534
|
+
uploadSummary.quiet = uploadSummary.quiet && quiet;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
uniqueFiles.forEach((file) => {
|
|
538
|
+
uploadSummary.assets.add(file);
|
|
539
|
+
if (getStorage(file) && !uploadSummary.pending.has(file)) {
|
|
540
|
+
uploadSummary.localCached.add(file);
|
|
541
|
+
}
|
|
542
|
+
if (uploadQueue[file] !== undefined) {
|
|
543
|
+
uploadSummary.pending.add(file);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!quiet && (pendingCount > 0 || logWhenIdle)) {
|
|
549
|
+
logUploadQueue(
|
|
550
|
+
{
|
|
551
|
+
provider: getProvider(),
|
|
552
|
+
total: uniqueFiles.length,
|
|
553
|
+
localCached: cachedCount,
|
|
554
|
+
pending: pendingCount,
|
|
555
|
+
},
|
|
556
|
+
false,
|
|
437
557
|
);
|
|
438
558
|
}
|
|
439
559
|
|
|
440
|
-
|
|
560
|
+
if (pendingCount > 0) {
|
|
561
|
+
return checkUpload();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (uploadRunner) {
|
|
565
|
+
return uploadRunner;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const result = {
|
|
569
|
+
provider: getProvider(),
|
|
570
|
+
total: uniqueFiles.length,
|
|
571
|
+
localCached: cachedCount,
|
|
572
|
+
pending: 0,
|
|
573
|
+
remoteCached: 0,
|
|
574
|
+
uploaded: 0,
|
|
575
|
+
failed: 0,
|
|
576
|
+
failedFiles: [],
|
|
577
|
+
durationMs: 0,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
if (logWhenIdle) {
|
|
581
|
+
logUploadResult(result, quiet);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return Promise.resolve(result);
|
|
441
585
|
}
|
|
442
586
|
|
|
443
587
|
module.exports = {
|