hw-weapp-compiler 1.0.17 → 1.0.18

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 CHANGED
@@ -1,76 +1,117 @@
1
- # weapp-compiler
1
+ # hw-weapp-compiler
2
2
 
3
+ 基于 webpack 5 的原生微信小程序构建工具,支持 JavaScript、Less、WXML、WXS、分包、npm 组件和可选的 OBS/OSS 静态资源上传。
3
4
 
4
- ## 版本改动
5
+ ## 环境要求
5
6
 
6
- 1. **OpenSSL 3.0 兼容**: Node.js >= 17 时自动注入 `--openssl-legacy-provider`
7
- 2. **wxml-loader 空值守卫**: `loadModule` 失败时不再崩溃
7
+ - Node.js 18~24
8
+ - 项目源码位于 `src/`
9
+ - 必须存在 `src/app.json` 和 `src/app.js`
8
10
 
9
11
  ## 安装
10
12
 
11
13
  ```bash
12
- npm i weapp-compiler@2.x -D
14
+ npm install hw-weapp-compiler@1 -D
15
+ ```
16
+
17
+ 在业务项目的 `package.json` 中添加:
18
+
19
+ ```json
20
+ {
21
+ "scripts": {
22
+ "dev": "hw-weapp dev",
23
+ "build": "hw-weapp build"
24
+ }
25
+ }
13
26
  ```
14
27
 
15
28
  ## 使用
16
29
 
17
30
  ```bash
18
- # 开发调试
19
- npm run dev # 测试环境
20
- npm run dev -s # 预发环境
21
- npm run dev -p # 生产环境
22
-
23
- # 生产发布
24
- npm run build -d # 测试环境
25
- npm run build -s # 预发环境
26
- npm run build # 生产环境
31
+ # 开发监听,默认 development
32
+ npm run dev
33
+
34
+ # 指定预发或生产业务环境
35
+ npm run dev -- --simulation
36
+ npm run dev -- --production
37
+
38
+ # 生产构建,默认 production
39
+ npm run build
40
+ npm run build -- --development
41
+ npm run build -- --simulation
27
42
  ```
28
43
 
44
+ 可用选项:
45
+
46
+ - `-d, --development`:测试环境
47
+ - `-s, --simulation`:预发环境
48
+ - `-p, --production`:生产环境
49
+ - `-a, --analyzer`:打开 webpack bundle analyzer
50
+ - `-q, --quiet`:关闭 webpack 进度输出
51
+
52
+ 最终环境会通过 `process.env.BUILD_ENV` 注入代码,并记录到 `dist/weapp.env.json`。
53
+
29
54
  ## 配置
30
55
 
31
- 项目根目录下创建 `.weapp.js`:
56
+ 在项目根目录创建 `.weapp.js`,最小配置为:
57
+
58
+ ```js
59
+ module.exports = {};
60
+ ```
61
+
62
+ 完整示例:
32
63
 
33
64
  ```js
34
65
  const path = require('path');
35
66
 
36
67
  module.exports = {
37
- // 路径别名
38
68
  alias: {
39
69
  '@utils': path.resolve(__dirname, 'src/utils'),
40
- '@config': path.resolve(__dirname, 'src/config'),
41
70
  },
42
- // 资源公共路径
43
71
  publicPath: 'https://cdn.example.com/weapp/',
44
- // 要同步的目录
45
- copyFiles: [{
46
- from: 'images',
47
- to: 'images',
48
- }],
49
- // OBS 或 OSS 配置(二选一)
50
- obsConfig: {
51
- access_key_id: 'XXX',
52
- secret_access_key: 'XXX',
53
- server: 'XXX',
54
- bucket: 'XXX',
55
- dir: 'weapp',
56
- },
57
- ossConfig: {
58
- region: '<Your region>',
59
- accessKeyId: '<Your AccessKeyId>',
60
- accessKeySecret: '<Your AccessKeySecret>',
61
- bucket: '<Your Bucket>',
62
- dir: 'weapp',
63
- },
72
+ copyFiles: [
73
+ { from: 'images', to: 'images' },
74
+ ],
75
+ };
76
+ ```
77
+
78
+ ## 静态资源上传
79
+
80
+ 上传是可选功能,只能配置 OBS 或 OSS 中的一种。生产构建会等待上传完成,任何上传失败都会让构建失败;编译失败时不会上传半成品。
81
+
82
+ 建议从环境变量读取凭据,避免把密钥提交到仓库:
83
+
84
+ ```js
85
+ module.exports = {
86
+ obsConfig: process.env.OBS_ACCESS_KEY_ID
87
+ ? {
88
+ access_key_id: process.env.OBS_ACCESS_KEY_ID,
89
+ secret_access_key: process.env.OBS_SECRET_ACCESS_KEY,
90
+ server: process.env.OBS_SERVER,
91
+ bucket: process.env.OBS_BUCKET,
92
+ dir: 'weapp',
93
+ }
94
+ : undefined,
64
95
  };
65
96
  ```
66
97
 
67
- `project.config.json` 中忽略构建产物:
98
+ OSS 配置字段为 `region`、`accessKeyId`、`accessKeySecret`、`bucket` 和 `dir`。
99
+
100
+ ## 开发监听限制
101
+
102
+ 修改已有文件可以正常增量构建。开发监听期间新增页面、组件、分包,或后创建同名 `.less`、`.wxss`、`.wxml`、`.json` 文件后,需要重新启动 `dev`。生产 `build` 每次都会重新读取完整项目配置。
103
+
104
+ ## 微信开发者工具配置
105
+
106
+ 如果资源使用 CDN 上传,可以在 `src/project.config.json` 中忽略本地产物:
68
107
 
69
108
  ```json
70
- "packOptions": {
71
- "ignore": [
72
- { "type": "folder", "value": "assets" },
73
- { "type": "regexp", "value": "\\.map$" }
74
- ]
109
+ {
110
+ "packOptions": {
111
+ "ignore": [
112
+ { "type": "folder", "value": "assets" },
113
+ { "type": "regexp", "value": "\\.map$" }
114
+ ]
115
+ }
75
116
  }
76
117
  ```
@@ -56,6 +56,9 @@ ossConfig: {
56
56
  `),
57
57
  );
58
58
  }
59
+ if (config.obsConfig && config.ossConfig) {
60
+ throw new TypeError('.weapp.js 不能同时配置 obsConfig 和 ossConfig');
61
+ }
59
62
  const alias = {
60
63
  ...(config.alias || {}),
61
64
  };
@@ -9,10 +9,25 @@ const { RawSource } = require('webpack-sources');
9
9
 
10
10
  const { addToUploadQueue } = require('../utils/upload');
11
11
  const { compatiblePath } = require('../utils/compatiblePath');
12
- const { PATTERNS, ASSET_EXT_PATTERN, ASSETS_DIR } = require('../config/constants');
12
+ const { ENV, PATTERNS, ASSET_EXT_PATTERN, ASSETS_DIR } = require('../config/constants');
13
13
 
14
14
  const pluginName = 'WeappCompilerPlugin';
15
15
 
16
+ async function uploadCompilationAssets(stats, compiler, assets, upload = addToUploadQueue) {
17
+ if (stats.hasErrors() || assets.length === 0) {
18
+ return;
19
+ }
20
+
21
+ try {
22
+ await upload(assets);
23
+ } catch (error) {
24
+ if (compiler.options.mode === ENV.PROD) {
25
+ throw error;
26
+ }
27
+ compiler.getInfrastructureLogger(pluginName).error(error);
28
+ }
29
+ }
30
+
16
31
  /**
17
32
  * 检测产物中存在哪些公共模块和分包模块
18
33
  */
@@ -76,13 +91,19 @@ function injectGlobalModules(content, assetName, flags) {
76
91
 
77
92
  // runtime 暴露到全局
78
93
  if (assetName === 'runtime.js') {
94
+ const runtimeCachePattern = /\b(var|let|const)\s+__webpack_module_cache__\s*=\s*\{\};/;
95
+ if (!runtimeCachePattern.test(result)) {
96
+ throw new Error(
97
+ '无法注入微信小程序 runtime:当前 webpack 产物中缺少 module cache 标记,请检查 webpack 版本',
98
+ );
99
+ }
79
100
  result = result.replace(
80
- 'var __webpack_module_cache__ = {};',
81
- `
101
+ runtimeCachePattern,
102
+ (statement, declaration) => `
82
103
  if (!global.__webpack_module_cache__) {
83
104
  global.__webpack_module_cache__ = {};
84
105
  }
85
- var __webpack_module_cache__ = global.__webpack_module_cache__;
106
+ ${declaration} __webpack_module_cache__ = global.__webpack_module_cache__;
86
107
  global.__webpack_require__ = __webpack_require__;
87
108
  `,
88
109
  );
@@ -167,9 +188,10 @@ class WeappPlugin {
167
188
  apply(compiler) {
168
189
  let obsAssets = [];
169
190
 
170
- compiler.hooks.done.tap(pluginName, () => {
171
- addToUploadQueue([...obsAssets]);
191
+ compiler.hooks.done.tapPromise(pluginName, async (stats) => {
192
+ const assets = Array.from(new Set(obsAssets));
172
193
  obsAssets = [];
194
+ await uploadCompilationAssets(stats, compiler, assets);
173
195
  });
174
196
 
175
197
  compiler.hooks.compilation.tap(pluginName, (compilation) => {
@@ -228,3 +250,7 @@ class WeappPlugin {
228
250
  }
229
251
 
230
252
  module.exports = WeappPlugin;
253
+ module.exports._internals = {
254
+ injectGlobalModules,
255
+ uploadCompilationAssets,
256
+ };
@@ -3,6 +3,21 @@ const path = require('path');
3
3
  const fse = require('fs-extra');
4
4
  const { ASSET_EXT_PATTERN, PATTERNS, SRC_DIR } = require('../config/constants');
5
5
 
6
+ function extractStyleUrls(style) {
7
+ const urls = [];
8
+ const urlPattern = /url\(\s*(?:(['"])(.*?)\1|([^)]*?))\s*\)/gi;
9
+ let match;
10
+
11
+ // eslint-disable-next-line no-cond-assign
12
+ while ((match = urlPattern.exec(style)) !== null) {
13
+ const value = (match[2] !== undefined ? match[2] : match[3]).trim();
14
+ if (value) {
15
+ urls.push(value);
16
+ }
17
+ }
18
+ return urls;
19
+ }
20
+
6
21
  function getWxmlAssets(filePath, content) {
7
22
  const resolvePath = (attr, dir) => {
8
23
  return new Promise((resolve) => {
@@ -32,7 +47,6 @@ function getWxmlAssets(filePath, content) {
32
47
  let allAttrs = [];
33
48
  const parser = new htmlparser2.Parser({
34
49
  onopentag: async (name, attributes) => {
35
- const reg = /url\(.*\)/g;
36
50
  const { style } = attributes;
37
51
 
38
52
  allAttrs = allAttrs.concat(
@@ -43,18 +57,7 @@ function getWxmlAssets(filePath, content) {
43
57
  );
44
58
 
45
59
  if (style) {
46
- const styles = attributes.style.split(';');
47
- styles.forEach((styleItem) => {
48
- (styleItem.match(reg) || []).forEach((item) => {
49
- allAttrs.push(
50
- item
51
- .replace(/^(url\(”)/g, '')
52
- .replace(/^(url\(')/g, '')
53
- .replace(/^(url\()/g, '')
54
- .replace(/(“\)|'\)|\))$/g, ''),
55
- );
56
- });
57
- });
60
+ allAttrs = allAttrs.concat(extractStyleUrls(style));
58
61
  }
59
62
  },
60
63
  onerror: (err) => {
@@ -64,7 +67,7 @@ function getWxmlAssets(filePath, content) {
64
67
  const filteredAttrs = allAttrs
65
68
  .filter((item) => !!item)
66
69
  .map((item) => item.split('?')[0])
67
- .filter((item) => !/^(http:|https:)/.test(item))
70
+ .filter((item) => !/^(?:https?:)?\/\//i.test(item))
68
71
  .filter((item) => !/{{.*}}/.test(item))
69
72
  .filter((item) => !/\+.*\+/.test(item));
70
73
 
@@ -106,4 +109,5 @@ function getWxmlAssets(filePath, content) {
106
109
 
107
110
  module.exports = {
108
111
  getWxmlAssets,
112
+ extractStyleUrls,
109
113
  };
@@ -18,8 +18,6 @@ const { obsConfig, ossConfig } = getConfig();
18
18
 
19
19
  let obsClient;
20
20
  let ossClient;
21
- let progress;
22
- let uploadQueue = {};
23
21
 
24
22
  function getObsClient() {
25
23
  if (!obsClient) {
@@ -110,77 +108,61 @@ function doUpload(file) {
110
108
  return doObsUpload(file);
111
109
  }
112
110
 
113
- function updateProgress() {
114
- const completed =
115
- Object.entries(uploadQueue).filter((item) => item[1] === 'completed').length + 1;
116
- const total = Object.keys(uploadQueue).length;
117
-
118
- if (!progress) {
119
- progress = new Progress('uploading assets [:bar] :current/:total', {
120
- total,
121
- width: 40,
122
- clear: true,
123
- });
124
- }
125
-
126
- progress.tick();
127
-
128
- if (completed === total) {
129
- progress.tick({
130
- current: total,
131
- });
132
- progress = null;
133
- uploadQueue = {};
111
+ async function uploadFile(file) {
112
+ try {
113
+ await getStat(file);
114
+ } catch (error) {
115
+ await doUpload(file);
134
116
  }
135
-
136
- return `[${completed}/${total}]`;
117
+ setStorage(file, true);
137
118
  }
138
119
 
139
- async function checkUpload() {
140
- const files = Object.entries(uploadQueue)
141
- .filter((item) => item[1] === false)
142
- .splice(0, 10);
120
+ async function uploadFiles(files, task = uploadFile) {
121
+ const failures = [];
122
+ const progress = new Progress('uploading assets [:bar] :current/:total', {
123
+ total: files.length,
124
+ width: 40,
125
+ clear: true,
126
+ });
143
127
 
144
- if (files && files.length) {
128
+ for (let index = 0; index < files.length; index += 10) {
129
+ const batch = files.slice(index, index + 10);
145
130
  await Promise.all(
146
- files.map(async (file) => {
147
- uploadQueue[file[0]] = 'uploading';
131
+ batch.map(async (file) => {
148
132
  try {
149
- await getStat(file[0]);
150
- setStorage(file[0], true);
151
- updateProgress();
133
+ await task(file);
152
134
  } catch (error) {
153
- try {
154
- await doUpload(file[0]);
155
- setStorage(file[0], true);
156
- } catch (uploadError) {
157
- console.error(`Failed to upload ${file[0]}:`, uploadError);
158
- }
159
- updateProgress();
135
+ failures.push({ file, error });
136
+ } finally {
137
+ progress.tick();
160
138
  }
161
- uploadQueue[file[0]] = 'completed';
162
139
  }),
163
140
  );
141
+ }
164
142
 
165
- checkUpload();
143
+ if (failures.length > 0) {
144
+ const details = failures
145
+ .map(({ file, error }) => `${file}: ${error && error.message ? error.message : error}`)
146
+ .join('\n');
147
+ const error = new Error(`Failed to upload ${failures.length} asset(s):\n${details}`);
148
+ error.failures = failures;
149
+ throw error;
166
150
  }
167
151
  }
168
152
 
169
- function addToUploadQueue(assets) {
170
- if (!obsConfig && !ossConfig) {
171
- console.warn('请配置obsConfig 或 ossConfig,否则无法上传文件到obs 或 oss');
153
+ async function addToUploadQueue(assets) {
154
+ if ((!obsConfig && !ossConfig) || assets.length === 0) {
172
155
  return;
173
156
  }
174
157
 
175
- assets.forEach((asset) => {
176
- const file = path.resolve(DIST_DIR, asset);
177
- if (uploadQueue[file] === undefined) {
178
- uploadQueue[file] = false;
179
- }
180
- });
181
- checkUpload();
158
+ const files = Array.from(new Set(assets.map((asset) => path.resolve(DIST_DIR, asset))));
159
+ await uploadFiles(files);
182
160
  }
183
161
 
184
162
  module.exports = {
185
163
  addToUploadQueue,
164
+ _internals: {
165
+ uploadFile,
166
+ uploadFiles,
167
+ },
186
168
  };
@@ -180,13 +180,13 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
180
180
  loader: MiniCssExtractPlugin.loader,
181
181
  },
182
182
  {
183
- loader: 'css-loader',
183
+ loader: require.resolve('css-loader'),
184
184
  options: {
185
185
  importLoaders: use.length + 1,
186
186
  },
187
187
  },
188
188
  {
189
- loader: 'postcss-loader',
189
+ loader: require.resolve('postcss-loader'),
190
190
  options: {
191
191
  postcssOptions: {
192
192
  plugins: [
@@ -239,14 +239,16 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
239
239
  });
240
240
  });
241
241
 
242
- plugins.push(
243
- new CopyPlugin({
244
- patterns,
245
- options: {
246
- concurrency: 100,
247
- },
248
- }),
249
- );
242
+ if (patterns.length > 0) {
243
+ plugins.push(
244
+ new CopyPlugin({
245
+ patterns,
246
+ options: {
247
+ concurrency: 100,
248
+ },
249
+ }),
250
+ );
251
+ }
250
252
 
251
253
  return merge(
252
254
  {
@@ -275,12 +277,12 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
275
277
  test: ASSET_EXT_PATTERN,
276
278
  use: [
277
279
  {
278
- loader: 'url-loader',
280
+ loader: require.resolve('url-loader'),
279
281
  options: {
280
282
  limit: 0,
281
283
  esModule: false,
282
284
  fallback: {
283
- loader: 'file-loader',
285
+ loader: require.resolve('file-loader'),
284
286
  options: {
285
287
  name: `${ASSETS_DIR}/[name].[hash].[ext]`,
286
288
  },
@@ -301,7 +303,7 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
301
303
  ...getCssLoader({
302
304
  use: [
303
305
  {
304
- loader: 'less-loader',
306
+ loader: require.resolve('less-loader'),
305
307
  options: {
306
308
  lessOptions() {
307
309
  return {
@@ -319,7 +321,7 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
319
321
  type: 'javascript/auto',
320
322
  use: [
321
323
  {
322
- loader: 'file-loader',
324
+ loader: require.resolve('file-loader'),
323
325
  options: {
324
326
  name: assetsName,
325
327
  },
@@ -331,7 +333,7 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
331
333
  type: 'javascript/auto',
332
334
  use: [
333
335
  {
334
- loader: 'file-loader',
336
+ loader: require.resolve('file-loader'),
335
337
  options: {
336
338
  name: assetsName,
337
339
  },
@@ -345,7 +347,7 @@ module.exports = async (options, { analyzer, quiet } = {}) => {
345
347
  test: /\.(wxml)$/i,
346
348
  use: [
347
349
  {
348
- loader: 'file-loader',
350
+ loader: require.resolve('file-loader'),
349
351
  options: {
350
352
  name: assetsName,
351
353
  },
package/index.js CHANGED
@@ -1,27 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // 高版本 Node.js (>=17) 兼容 OpenSSL 3.0
4
- const nodeMajor = process.versions.node.split('.')[0];
5
- if (nodeMajor >= 17) {
6
- process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ''} --openssl-legacy-provider`.trim();
7
- }
8
-
9
3
  const { program } = require('commander');
10
4
  const path = require('path');
11
5
  const fs = require('fs');
12
- const dev = require('./build/dev');
13
- const prod = require('./build/prod');
14
6
  const { setBuildEnv } = require('./build/utils/buildEnv');
15
7
  const { ENV } = require('./build/config/constants');
16
8
 
17
9
  const { version } = JSON.parse(
18
10
  fs.readFileSync(path.resolve(__dirname, './package.json')).toString(),
19
11
  );
20
- const compiler = {
21
- dev,
22
- prod,
23
- };
24
-
25
12
  program.version(version);
26
13
  program.option('-a, --analyzer', 'webpack-bundle-analyzer');
27
14
  program.option('-s, --simulation', 'process.env.BUILD_ENV = simulation');
@@ -29,25 +16,27 @@ program.option('-d, --development', 'process.env.BUILD_ENV = development');
29
16
  program.option('-p, --production', 'process.env.BUILD_ENV = production');
30
17
  program.option('-q, --quiet', '安静模式,打印减少');
31
18
 
32
- program.command('dev').action(() => {
19
+ program.command('dev').action(async () => {
33
20
  setBuildEnv({
34
21
  mode: ENV.DEV,
35
22
  ...program.opts(),
36
23
  });
37
- compiler.dev(program.opts()).catch((err) => {
38
- console.error(err);
39
- process.exitCode = 1;
40
- });
24
+ // 延迟加载构建模块,保证 --help/--version 不依赖业务项目配置。
25
+ // eslint-disable-next-line global-require
26
+ const dev = require('./build/dev');
27
+ await dev(program.opts());
41
28
  });
42
- program.command('build').action(() => {
29
+ program.command('build').action(async () => {
43
30
  setBuildEnv({
44
31
  mode: ENV.PROD,
45
32
  ...program.opts(),
46
33
  });
47
- compiler.prod(program.opts()).catch((err) => {
48
- console.error(err);
49
- process.exitCode = 1;
50
- });
34
+ // eslint-disable-next-line global-require
35
+ const prod = require('./build/prod');
36
+ await prod(program.opts());
51
37
  });
52
38
 
53
- program.parse(process.argv);
39
+ program.parseAsync(process.argv).catch((err) => {
40
+ console.error(err);
41
+ process.exitCode = 1;
42
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hw-weapp-compiler",
3
- "version": "1.0.17",
4
- "description": "基于 webpack5 的小程序构建工具,兼容 Node.js 14~24",
3
+ "version": "1.0.18",
4
+ "description": "基于 webpack5 的小程序构建工具,兼容 Node.js 18~24",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "hw-weapp": "index.js"
@@ -12,7 +12,7 @@
12
12
  "registry": "https://registry.npmjs.org"
13
13
  },
14
14
  "engines": {
15
- "node": ">=14.17.1"
15
+ "node": ">=18"
16
16
  },
17
17
  "dependencies": {
18
18
  "@swc/core": "^1.15.43",
@@ -30,7 +30,7 @@
30
30
  "file-loader": "^6.2.0",
31
31
  "fs-extra": "^9.1.0",
32
32
  "htmlparser2": "^6.0.1",
33
- "less": "^4.1.1",
33
+ "less": "4.6.7",
34
34
  "less-loader": "^8.0.0",
35
35
  "loader-utils": "^2.0.0",
36
36
  "mini-css-extract-plugin": "^1.6.0",
@@ -41,7 +41,7 @@
41
41
  "terser-webpack-plugin": "^5.6.1",
42
42
  "throttle-debounce": "^3.0.1",
43
43
  "url-loader": "^4.1.1",
44
- "webpack": "^5.61.0",
44
+ "webpack": "5.108.4",
45
45
  "webpack-bundle-analyzer": "^4.4.0",
46
46
  "webpack-merge": "^5.7.3",
47
47
  "webpack-sources": "^2.2.0"
@@ -52,6 +52,7 @@
52
52
  "scripts": {
53
53
  "build": "node ./index.js build",
54
54
  "dev": "nodemon --inspect --trace-deprecation --watch build ./index.js dev",
55
- "simulation": "node ./index.js build -s"
55
+ "simulation": "node ./index.js build -s",
56
+ "test": "node --test"
56
57
  }
57
58
  }