@arkxio/ark-dev-utils 0.1.6 → 0.1.9

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.
@@ -1,7 +1,7 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('os'), require('path'), require('util'), require('jsdom'), require('webpack')) :
3
3
  typeof define === 'function' && define.amd ? define(['exports', 'fs', 'os', 'path', 'util', 'jsdom', 'webpack'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["ark-dev-utils"] = {}, global.fs, global.os, global.path, global.util, global.jsdom, global.webpack));
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.index = {}, global.fs, global.os, global.path, global.util, global.jsdom, global.webpack));
5
5
  })(this, (function (exports, fs, os, path, util, jsdom, webpack) { 'use strict';
6
6
 
7
7
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
@@ -32,168 +32,168 @@
32
32
  var util__namespace = /*#__PURE__*/_interopNamespace(util);
33
33
  var jsdom__default = /*#__PURE__*/_interopDefaultLegacy(jsdom);
34
34
 
35
- /**
36
- * @type {{
37
- * ARK_DIST_DIR: 'ark_dist',
38
- * ARK_PROXY_DIR: 'ark_proxy',
39
- * HEL_BUNDLE_DIR: 'ark_bundle',
40
- * DEFAULT_GUESS_SUB_APP_CONF_PATH:string,
41
- * DEFAULT_PLAT: 'unpkg',
42
- * DEFAULT_NPM_CDN_TYPE: 'unpkg',
43
- * DEFAULT_SEMVER_API: true,
44
- * DEFAULT_HTML_INDEX_NAME: 'index.html',
45
- * PLUGIN_VER:string,
46
- * }}
47
- */
48
- var cst = {
49
- ARK_DIST_DIR: 'ark_dist',
50
- ARK_PROXY_DIR: 'ark_proxy',
51
- HEL_BUNDLE_DIR: 'ark_bundle',
52
- DEFAULT_GUESS_SUB_APP_CONF_PATH: '../../../src/configs/subApp',
53
- DEFAULT_PLAT: 'unpkg',
54
- DEFAULT_NPM_CDN_TYPE: 'unpkg',
55
- DEFAULT_HTML_INDEX_NAME: 'index.html',
56
- DEFAULT_SEMVER_API: true,
57
- PLUGIN_VER: '4.3.20',
35
+ /**
36
+ * @type {{
37
+ * ARK_DIST_DIR: 'ark_dist',
38
+ * ARK_PROXY_DIR: 'ark_proxy',
39
+ * HEL_BUNDLE_DIR: 'ark_bundle',
40
+ * DEFAULT_GUESS_SUB_APP_CONF_PATH:string,
41
+ * DEFAULT_PLAT: 'unpkg',
42
+ * DEFAULT_NPM_CDN_TYPE: 'unpkg',
43
+ * DEFAULT_SEMVER_API: true,
44
+ * DEFAULT_HTML_INDEX_NAME: 'index.html',
45
+ * PLUGIN_VER:string,
46
+ * }}
47
+ */
48
+ var cst = {
49
+ ARK_DIST_DIR: 'ark_dist',
50
+ ARK_PROXY_DIR: 'ark_proxy',
51
+ HEL_BUNDLE_DIR: 'ark_bundle',
52
+ DEFAULT_GUESS_SUB_APP_CONF_PATH: '../../../src/configs/subApp',
53
+ DEFAULT_PLAT: 'unpkg',
54
+ DEFAULT_NPM_CDN_TYPE: 'unpkg',
55
+ DEFAULT_HTML_INDEX_NAME: 'index.html',
56
+ DEFAULT_SEMVER_API: true,
57
+ PLUGIN_VER: '4.3.20',
58
58
  };
59
59
 
60
- /** @typedef {import('../../typings').FileDesc} FileDesc*/
61
-
62
- function verbose(...args) {
63
- console.log('[Ark-verbose:] ', ...args);
64
- }
65
-
66
- const cdnType2host = {
67
- unpkg: 'https://unpkg.com',
68
- jsdelivr: 'https://cdn.jsdelivr.net/npm',
69
- };
70
-
71
- function getNpmCdnHomePage(packageJson, options) {
72
- const { npmCdnType = cst.DEFAULT_NPM_CDN_TYPE, distDir = cst.ARK_DIST_DIR, homePage } = options;
73
- const { name, version } = packageJson;
74
- // 优先考虑用户透传的 homePage,表示用户部署了 unpkg 私服
75
- const unpkgHost = homePage || cdnType2host[npmCdnType] || '';
76
- // TODO,未来考虑更多类型的 cdn,如:jsdelivr
77
- return `${unpkgHost}/${name}@${version}/${distDir}`;
78
- }
79
-
80
- /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
81
-
82
- /**
83
- * @param {string} inputPath
84
- * @param {Object} options
85
- * @param {'end' | 'start'} [options.loc='end']
86
- * @param {boolean} [options.need=false]
87
- */
88
- function ensureSlash(inputPath, options) {
89
- const { need, loc = 'end' } = options;
90
- const isEnd = loc === 'end';
91
- const hasSlash = isEnd ? inputPath.endsWith('/') : inputPath.startsWith('/');
92
-
93
- const shouldDelSlash = hasSlash && !need;
94
- const shouldAddSlash = !hasSlash && need;
95
- if (isEnd) {
96
- if (shouldDelSlash) {
97
- return inputPath.substring(0, inputPath.length - 1);
98
- }
99
- if (shouldAddSlash) {
100
- return `${inputPath}/`;
101
- }
102
- }
103
- if (!isEnd) {
104
- if (shouldDelSlash) {
105
- // del start slash
106
- return inputPath.substring(1);
107
- }
108
- if (shouldAddSlash) {
109
- return `/${inputPath}`;
110
- }
111
- }
112
-
113
- return inputPath;
114
- }
115
-
116
- /** 语义化 slash 相关操作,方便上层理解和使用 */
117
- const slash = {
118
- start: (path) => ensureSlash(path, { loc: 'start', need: true }),
119
- noStart: (path) => ensureSlash(path, { loc: 'start', need: false }),
120
- end: (path) => ensureSlash(path, { loc: 'end', need: true }),
121
- noEnd: (path) => ensureSlash(path, { loc: 'end', need: false }),
122
- };
123
-
124
- function getHelProcessEnvParams() {
125
- // 以下常量由蓝盾流水线注入(由流水线变量或bash脚本注入)
126
- const {
127
- HOST,
128
- PORT,
129
- // appHomePage, 形如 http://xxx.cdn.com/hel/app1_2020121201011666
130
- HEL_APP_HOME_PAGE,
131
- /** 在构建机环境时,会注入真正对应的应用名 */
132
- HEL_APP_GROUP_NAME,
133
- HEL_APP_NAME,
134
- } = process.env;
135
-
136
- let appHomePage = HEL_APP_HOME_PAGE;
137
- // 未传递 HEL_APP_HOME_PAGE 的话,根据 HOST 和 PORT 推导
138
- if (!appHomePage && (HOST || PORT)) {
139
- const host = HOST || 'localhost';
140
- const port = PORT || '80';
141
- const hotsStr = host.startsWith('http') ? host : `http://${host}`;
142
- appHomePage = `${hotsStr}:${port}`;
143
- }
144
-
145
- return {
146
- appHomePage,
147
- appGroupName: HEL_APP_GROUP_NAME,
148
- appName: HEL_APP_NAME,
149
- };
150
- }
151
-
152
- /**
153
- * @param {Record<string, any>} pkg
154
- * @param {ICreateSubAppOptions} options
155
- * @returns
156
- */
157
- function getHelEnvParams(pkg, options = {}) {
158
- const { platform, distDir, homePage: userCustomHomePage, handleHomePage = true, npmCdnType } = options;
159
- let cdnHomePage = '';
160
- // 计算 unpkg 平台 的 homePage 值,此时如果透传了 homePage,表示 unpkg 为私服
161
- if (platform === 'unpkg' && handleHomePage) {
162
- cdnHomePage = getNpmCdnHomePage(pkg, { distDir, npmCdnType, homePage: userCustomHomePage });
163
- }
164
-
165
- // 来自 process.env 的值优先级最高
166
- const p0EnvParams = getHelProcessEnvParams();
167
- const appName = p0EnvParams.appName || pkg.appGroupName || '';
168
- return {
169
- appHomePage: p0EnvParams.appHomePage || cdnHomePage || userCustomHomePage || pkg.homepage || '/',
170
- appGroupName: p0EnvParams.appGroupName || pkg.appGroupName || appName,
171
- appName,
172
- };
173
- }
174
-
175
- /**
176
- * @param {string} appName - hel 管理台注册的应用名 或 package.name
177
- * @param {boolean} [useTimestampSuffix=true] - default: true, 是否设置时间戳后缀
178
- * 设置为 true 时,支持同一个模块正确执行多版本js文件
179
- * 如不需要同屏加载同一个模块的多个版本功能,且需要自己定制一些其他逻辑,可以设置为false
180
- */
181
- function getJsonpFnName(appName, useTimestampSuffix = true) {
182
- if (useTimestampSuffix) {
183
- return `helJsonp_${appName}_${Date.now()}`;
184
- }
185
-
186
- return `helJsonp_${appName}`;
60
+ /** @typedef {import('../../typings').FileDesc} FileDesc*/
61
+
62
+ function verbose(...args) {
63
+ console.log('[Ark-verbose:] ', ...args);
64
+ }
65
+
66
+ const cdnType2host = {
67
+ unpkg: 'https://unpkg.com',
68
+ jsdelivr: 'https://cdn.jsdelivr.net/npm',
69
+ };
70
+
71
+ function getNpmCdnHomePage(packageJson, options) {
72
+ const { npmCdnType = cst.DEFAULT_NPM_CDN_TYPE, distDir = cst.ARK_DIST_DIR, homePage } = options;
73
+ const { name, version } = packageJson;
74
+ // 优先考虑用户透传的 homePage,表示用户部署了 unpkg 私服
75
+ const unpkgHost = homePage || cdnType2host[npmCdnType] || '';
76
+ // TODO,未来考虑更多类型的 cdn,如:jsdelivr
77
+ return `${unpkgHost}/${name}@${version}/${distDir}`;
187
78
  }
188
79
 
189
- /**
190
- *
191
- * @param {string} homePage - 应用homePage
192
- * @param {boolean} [needSlash]
193
- * @returns
194
- */
195
- function getPublicPathOrUrl(homePage, needSlash = true) {
196
- return ensureSlash(homePage, { loc: 'end', need: needSlash });
80
+ /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
81
+
82
+ /**
83
+ * @param {string} inputPath
84
+ * @param {Object} options
85
+ * @param {'end' | 'start'} [options.loc='end']
86
+ * @param {boolean} [options.need=false]
87
+ */
88
+ function ensureSlash(inputPath, options) {
89
+ const { need, loc = 'end' } = options;
90
+ const isEnd = loc === 'end';
91
+ const hasSlash = isEnd ? inputPath.endsWith('/') : inputPath.startsWith('/');
92
+
93
+ const shouldDelSlash = hasSlash && !need;
94
+ const shouldAddSlash = !hasSlash && need;
95
+ if (isEnd) {
96
+ if (shouldDelSlash) {
97
+ return inputPath.substring(0, inputPath.length - 1);
98
+ }
99
+ if (shouldAddSlash) {
100
+ return `${inputPath}/`;
101
+ }
102
+ }
103
+ if (!isEnd) {
104
+ if (shouldDelSlash) {
105
+ // del start slash
106
+ return inputPath.substring(1);
107
+ }
108
+ if (shouldAddSlash) {
109
+ return `/${inputPath}`;
110
+ }
111
+ }
112
+
113
+ return inputPath;
114
+ }
115
+
116
+ /** 语义化 slash 相关操作,方便上层理解和使用 */
117
+ const slash = {
118
+ start: (path) => ensureSlash(path, { loc: 'start', need: true }),
119
+ noStart: (path) => ensureSlash(path, { loc: 'start', need: false }),
120
+ end: (path) => ensureSlash(path, { loc: 'end', need: true }),
121
+ noEnd: (path) => ensureSlash(path, { loc: 'end', need: false }),
122
+ };
123
+
124
+ function getHelProcessEnvParams() {
125
+ // 以下常量由蓝盾流水线注入(由流水线变量或bash脚本注入)
126
+ const {
127
+ HOST,
128
+ PORT,
129
+ // appHomePage, 形如 http://xxx.cdn.com/hel/app1_2020121201011666
130
+ HEL_APP_HOME_PAGE,
131
+ /** 在构建机环境时,会注入真正对应的应用名 */
132
+ HEL_APP_GROUP_NAME,
133
+ HEL_APP_NAME,
134
+ } = process.env;
135
+
136
+ let appHomePage = HEL_APP_HOME_PAGE;
137
+ // 未传递 HEL_APP_HOME_PAGE 的话,根据 HOST 和 PORT 推导
138
+ if (!appHomePage && (HOST || PORT)) {
139
+ const host = HOST || 'localhost';
140
+ const port = PORT || '80';
141
+ const hotsStr = host.startsWith('http') ? host : `http://${host}`;
142
+ appHomePage = `${hotsStr}:${port}`;
143
+ }
144
+
145
+ return {
146
+ appHomePage,
147
+ appGroupName: HEL_APP_GROUP_NAME,
148
+ appName: HEL_APP_NAME,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * @param {Record<string, any>} pkg
154
+ * @param {ICreateSubAppOptions} options
155
+ * @returns
156
+ */
157
+ function getHelEnvParams(pkg, options = {}) {
158
+ const { platform, distDir, homePage: userCustomHomePage, handleHomePage = true, npmCdnType } = options;
159
+ let cdnHomePage = '';
160
+ // 计算 unpkg 平台 的 homePage 值,此时如果透传了 homePage,表示 unpkg 为私服
161
+ if (platform === 'unpkg' && handleHomePage) {
162
+ cdnHomePage = getNpmCdnHomePage(pkg, { distDir, npmCdnType, homePage: userCustomHomePage });
163
+ }
164
+
165
+ // 来自 process.env 的值优先级最高
166
+ const p0EnvParams = getHelProcessEnvParams();
167
+ const appName = p0EnvParams.appName || pkg.appGroupName || '';
168
+ return {
169
+ appHomePage: p0EnvParams.appHomePage || cdnHomePage || userCustomHomePage || pkg.homepage || '/',
170
+ appGroupName: p0EnvParams.appGroupName || pkg.appGroupName || appName,
171
+ appName,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * @param {string} appName - hel 管理台注册的应用名 或 package.name
177
+ * @param {boolean} [useTimestampSuffix=true] - default: true, 是否设置时间戳后缀
178
+ * 设置为 true 时,支持同一个模块正确执行多版本js文件
179
+ * 如不需要同屏加载同一个模块的多个版本功能,且需要自己定制一些其他逻辑,可以设置为false
180
+ */
181
+ function getJsonpFnName(appName, useTimestampSuffix = true) {
182
+ if (useTimestampSuffix) {
183
+ return `helJsonp_${appName}_${Date.now()}`;
184
+ }
185
+
186
+ return `helJsonp_${appName}`;
187
+ }
188
+
189
+ /**
190
+ *
191
+ * @param {string} homePage - 应用homePage
192
+ * @param {boolean} [needSlash]
193
+ * @returns
194
+ */
195
+ function getPublicPathOrUrl(homePage, needSlash = true) {
196
+ return ensureSlash(homePage, { loc: 'end', need: needSlash });
197
197
  }
198
198
 
199
199
  var baseUtils = /*#__PURE__*/Object.freeze({
@@ -206,1119 +206,1109 @@
206
206
  getPublicPathOrUrl: getPublicPathOrUrl
207
207
  });
208
208
 
209
- /**
210
- * for getting pretty format multi line string when use \`...\`
211
- * this function will remove indent of every line automatically
212
- * @param {string} mayLineBreakStr
213
- * @param {'MULTI' | 'ONE'} [mode='MULTI']
214
- * @returns
215
- * ```
216
- * // usage 1, process multi lines with mode='MULTI' by default
217
- * pfstr(`
218
- * line1 line1 line1,
219
- * line2 line2 line2.
220
- * `);
221
- * // pass to console.log will print:
222
- * line1 line1 line1,
223
- * line2 line2 line2.
224
- * // attention: end <br/> will be removed automatically in MULTI mode
225
- * pfstr(`
226
- * line1 line1 line1,<br/>
227
- * line2 line2 line2.
228
- * `);
229
- * // pass to console.log will print:
230
- * line1 line1 line1,
231
- * line2 line2 line2.
232
- *
233
- * // usage 2, set mode='ONE' to get no line break string
234
- * pfstr(`
235
- * line1 line1 line1,
236
- * line2 line2 line2.
237
- * `, 'ONE');
238
- * // pass to console.log will print:
239
- * line1 line1 line1, line2 line2 line2.
240
- *
241
- * // usage 3, add <br/> to control line break
242
- * pfstr(`
243
- * line1 line1 line1,
244
- * line2 line2 line2,<br/>
245
- * line3 line3 line3.
246
- * `, 'ONE');
247
- * // pass to console.log will print:
248
- * line1 line1 line1, line2 line2 line2,
249
- * line3 line3 line3.
250
- * ```
251
- */
252
- function pfstr(/** @type string */ mayLineBreakStr, mode = 'MULTI') {
253
- // MULTI ONE
254
- const lines = mayLineBreakStr.split('\n');
255
- const lastIdx = lines.length - 1;
256
-
257
- const formatLine = lines
258
- .map((line, idx) => {
259
- if (!line && (idx === 0 || idx === lastIdx)) {
260
- return '';
261
- }
262
- const replaceBr = (/** @type string */ line, hasBrStr, noBrStr = '') => {
263
- let result = line;
264
- if (line.endsWith('<br/>')) {
265
- result = line.substring(0, result.length - 5);
266
- return `${result}${hasBrStr}`;
267
- }
268
- return `${result}${noBrStr}`;
269
- };
270
-
271
- // 此处暂时规避可选链写法,因 rollup 对此未处理,编译后上层使用会报错
272
- // SyntaxError: Unexpected token '.'
273
- const { trimStart } = line;
274
- let result = trimStart ? line.trimStart() : line; // 去头部所有空格
275
- if (mode === 'MULTI') {
276
- return `${replaceBr(result, '')}\n`;
277
- }
278
- result = replaceBr(result, '\n', ' ');
279
- return result;
280
- })
281
- .join('');
282
- return formatLine;
209
+ /**
210
+ * for getting pretty format multi line string when use \`...\`
211
+ * this function will remove indent of every line automatically
212
+ * @param {string} mayLineBreakStr
213
+ * @param {'MULTI' | 'ONE'} [mode='MULTI']
214
+ * @returns
215
+ * ```
216
+ * // usage 1, process multi lines with mode='MULTI' by default
217
+ * pfstr(`
218
+ * line1 line1 line1,
219
+ * line2 line2 line2.
220
+ * `);
221
+ * // pass to console.log will print:
222
+ * line1 line1 line1,
223
+ * line2 line2 line2.
224
+ * // attention: end <br/> will be removed automatically in MULTI mode
225
+ * pfstr(`
226
+ * line1 line1 line1,<br/>
227
+ * line2 line2 line2.
228
+ * `);
229
+ * // pass to console.log will print:
230
+ * line1 line1 line1,
231
+ * line2 line2 line2.
232
+ *
233
+ * // usage 2, set mode='ONE' to get no line break string
234
+ * pfstr(`
235
+ * line1 line1 line1,
236
+ * line2 line2 line2.
237
+ * `, 'ONE');
238
+ * // pass to console.log will print:
239
+ * line1 line1 line1, line2 line2 line2.
240
+ *
241
+ * // usage 3, add <br/> to control line break
242
+ * pfstr(`
243
+ * line1 line1 line1,
244
+ * line2 line2 line2,<br/>
245
+ * line3 line3 line3.
246
+ * `, 'ONE');
247
+ * // pass to console.log will print:
248
+ * line1 line1 line1, line2 line2 line2,
249
+ * line3 line3 line3.
250
+ * ```
251
+ */
252
+ function pfstr(/** @type string */ mayLineBreakStr, mode = 'MULTI') {
253
+ // MULTI ONE
254
+ const lines = mayLineBreakStr.split('\n');
255
+ const lastIdx = lines.length - 1;
256
+
257
+ const formatLine = lines
258
+ .map((line, idx) => {
259
+ if (!line && (idx === 0 || idx === lastIdx)) {
260
+ return '';
261
+ }
262
+ const replaceBr = (/** @type string */ line, hasBrStr, noBrStr = '') => {
263
+ let result = line;
264
+ if (line.endsWith('<br/>')) {
265
+ result = line.substring(0, result.length - 5);
266
+ return `${result}${hasBrStr}`;
267
+ }
268
+ return `${result}${noBrStr}`;
269
+ };
270
+
271
+ // 此处暂时规避可选链写法,因 rollup 对此未处理,编译后上层使用会报错
272
+ // SyntaxError: Unexpected token '.'
273
+ const { trimStart } = line;
274
+ let result = trimStart ? line.trimStart() : line; // 去头部所有空格
275
+ if (mode === 'MULTI') {
276
+ return `${replaceBr(result, '')}\n`;
277
+ }
278
+ result = replaceBr(result, '\n', ' ');
279
+ return result;
280
+ })
281
+ .join('');
282
+ return formatLine;
283
283
  }
284
284
 
285
- /*
286
- |--------------------------------------------------------------------------
287
- |
288
- | 此脚本在流水线上会被触发,用于校验组名是否和应用里的组名保持一致
289
- |
290
- |--------------------------------------------------------------------------
291
- */
292
-
293
- /**
294
- * @param {Record<string, any>} pkg - 用户模块的 package.json 文件
295
- * @param {ICheckOptions['fileFullPath'] | ICheckOptions} [subAppFilePathOrOptions] - 文件全路径名字,可带或不带后缀(.ts, .js)
296
- */
297
- function check (pkg, subAppFilePathOrOptions) {
298
- let fileFullPath = path__namespace.join(__dirname, cst.DEFAULT_GUESS_SUB_APP_CONF_PATH);
299
- let checkEnv = true;
300
- if (subAppFilePathOrOptions) {
301
- if (typeof subAppFilePathOrOptions === 'string') {
302
- fileFullPath = subAppFilePathOrOptions;
303
- } else {
304
- fileFullPath = subAppFilePathOrOptions.fileFullPath;
305
- if (subAppFilePathOrOptions.checkEnv !== undefined) {
306
- checkEnv = subAppFilePathOrOptions.checkEnv;
307
- }
308
- }
309
- }
310
-
311
- /** 子应用组名 */
312
- let srcAppGroupName = '';
313
- let content = '';
314
- if (fileFullPath.endsWith('.js') || fileFullPath.endsWith('.ts')) {
315
- content = fs__namespace.readFileSync(fileFullPath);
316
- } else {
317
- const fileFullPathJs = `${fileFullPath}.js`;
318
- const fileFullPathTs = `${fileFullPath}.ts`;
319
- const isJsExist = fs__namespace.existsSync(fileFullPathJs);
320
- if (isJsExist) {
321
- verbose(`guess js subApp file: ${fileFullPathJs}`);
322
- content = fs__namespace.readFileSync(fileFullPathJs);
323
- } else {
324
- verbose(`guess ts subApp file: ${fileFullPathTs}`);
325
- const isTsExist = fs__namespace.existsSync(fileFullPathTs);
326
- if (isTsExist) {
327
- content = fs__namespace.readFileSync(fileFullPathTs);
328
- } else {
329
- throw new Error('no src/configs/subApp.(js|ts) file found');
330
- }
331
- }
332
- }
333
-
334
- const fileStr = content.toString();
335
- const strList = fileStr.split(os__namespace.EOL);
336
- for (const item of strList) {
337
- // export const HEL_APP_GROUP_NAME = 'your-app';
338
- // or
339
- // export const LIB_NAME = 'your-app';
340
- if (item.includes('export') && item.includes('const') && (item.includes('LIB_NAME') || item.includes('HEL_APP_GROUP_NAME'))) {
341
- const noBlankStr = item.replace(/ /gi, '');
342
- let [, appNameStr] = noBlankStr.split('=');
343
- appNameStr = appNameStr.replace(/'/gi, '');
344
- appNameStr = appNameStr.replace(/"/gi, '');
345
- appNameStr = appNameStr.replace(/;/gi, '');
346
- appNameStr = appNameStr.replace(/\r/gi, '');
347
- appNameStr = appNameStr.replace(/\n/gi, '');
348
- srcAppGroupName = appNameStr;
349
- break;
350
- }
351
- }
352
- if (!srcAppGroupName) {
353
- throw new Error(
354
- pfstr(`
355
- HEL_APP_GROUP_NAME or LIB_NAME not found in src/configs/subApp.(js|ts) file,
356
- your should expose it like below:
357
- export const HEL_APP_GROUP_NAME = 'your-app';
358
- or
359
- export const LIB_NAME = 'your-app';
360
- `),
361
- );
362
- }
363
-
364
- const pgkAppGroupName = pkg.appGroupName || pkg.name;
365
- if (pgkAppGroupName !== srcAppGroupName) {
366
- throw new Error(
367
- `package.json name [${pgkAppGroupName}] should be equal to src/configs/subApp LIB_NAME(or HEL_APP_GROUP_NAME) [${srcAppGroupName}]`,
368
- );
369
- }
370
-
371
- if (checkEnv) {
372
- // 以下常量由 ci&cd 系统注入(通常由流水线变量或 bash 脚本注入)
373
- const { HEL_APP_GROUP_NAME } = process.env;
374
-
375
- if (pgkAppGroupName !== HEL_APP_GROUP_NAME) {
376
- throw new Error(`package.json name [${pgkAppGroupName}] should be equal to pipeline var HEL_APP_GROUP_NAME [${HEL_APP_GROUP_NAME}]`);
377
- }
378
- }
285
+ /*
286
+ |--------------------------------------------------------------------------
287
+ |
288
+ | 此脚本在流水线上会被触发,用于校验组名是否和应用里的组名保持一致
289
+ |
290
+ |--------------------------------------------------------------------------
291
+ */
292
+
293
+ /**
294
+ * @param {Record<string, any>} pkg - 用户模块的 package.json 文件
295
+ * @param {ICheckOptions['fileFullPath'] | ICheckOptions} [subAppFilePathOrOptions] - 文件全路径名字,可带或不带后缀(.ts, .js)
296
+ */
297
+ function check (pkg, subAppFilePathOrOptions) {
298
+ let fileFullPath = path__namespace.join(__dirname, cst.DEFAULT_GUESS_SUB_APP_CONF_PATH);
299
+ let checkEnv = true;
300
+ if (subAppFilePathOrOptions) {
301
+ if (typeof subAppFilePathOrOptions === 'string') {
302
+ fileFullPath = subAppFilePathOrOptions;
303
+ } else {
304
+ fileFullPath = subAppFilePathOrOptions.fileFullPath;
305
+ if (subAppFilePathOrOptions.checkEnv !== undefined) {
306
+ checkEnv = subAppFilePathOrOptions.checkEnv;
307
+ }
308
+ }
309
+ }
310
+
311
+ /** 子应用组名 */
312
+ let srcAppGroupName = '';
313
+ let content = '';
314
+ if (fileFullPath.endsWith('.js') || fileFullPath.endsWith('.ts')) {
315
+ content = fs__namespace.readFileSync(fileFullPath);
316
+ } else {
317
+ const fileFullPathJs = `${fileFullPath}.js`;
318
+ const fileFullPathTs = `${fileFullPath}.ts`;
319
+ const isJsExist = fs__namespace.existsSync(fileFullPathJs);
320
+ if (isJsExist) {
321
+ verbose(`guess js subApp file: ${fileFullPathJs}`);
322
+ content = fs__namespace.readFileSync(fileFullPathJs);
323
+ } else {
324
+ verbose(`guess ts subApp file: ${fileFullPathTs}`);
325
+ const isTsExist = fs__namespace.existsSync(fileFullPathTs);
326
+ if (isTsExist) {
327
+ content = fs__namespace.readFileSync(fileFullPathTs);
328
+ } else {
329
+ throw new Error('no src/configs/subApp.(js|ts) file found');
330
+ }
331
+ }
332
+ }
333
+
334
+ const fileStr = content.toString();
335
+ const strList = fileStr.split(os__namespace.EOL);
336
+ for (const item of strList) {
337
+ // export const HEL_APP_GROUP_NAME = 'your-app';
338
+ // or
339
+ // export const LIB_NAME = 'your-app';
340
+ if (item.includes('export') && item.includes('const') && (item.includes('LIB_NAME') || item.includes('HEL_APP_GROUP_NAME'))) {
341
+ const noBlankStr = item.replace(/ /gi, '');
342
+ let [, appNameStr] = noBlankStr.split('=');
343
+ appNameStr = appNameStr.replace(/'/gi, '');
344
+ appNameStr = appNameStr.replace(/"/gi, '');
345
+ appNameStr = appNameStr.replace(/;/gi, '');
346
+ appNameStr = appNameStr.replace(/\r/gi, '');
347
+ appNameStr = appNameStr.replace(/\n/gi, '');
348
+ srcAppGroupName = appNameStr;
349
+ break;
350
+ }
351
+ }
352
+ if (!srcAppGroupName) {
353
+ throw new Error(
354
+ pfstr(`
355
+ HEL_APP_GROUP_NAME or LIB_NAME not found in src/configs/subApp.(js|ts) file,
356
+ your should expose it like below:
357
+ export const HEL_APP_GROUP_NAME = 'your-app';
358
+ or
359
+ export const LIB_NAME = 'your-app';
360
+ `),
361
+ );
362
+ }
363
+
364
+ const pgkAppGroupName = pkg.appGroupName || pkg.name;
365
+ if (pgkAppGroupName !== srcAppGroupName) {
366
+ throw new Error(
367
+ `package.json name [${pgkAppGroupName}] should be equal to src/configs/subApp LIB_NAME(or HEL_APP_GROUP_NAME) [${srcAppGroupName}]`,
368
+ );
369
+ }
370
+
371
+ if (checkEnv) {
372
+ // 以下常量由 ci&cd 系统注入(通常由流水线变量或 bash 脚本注入)
373
+ const { HEL_APP_GROUP_NAME } = process.env;
374
+
375
+ if (pgkAppGroupName !== HEL_APP_GROUP_NAME) {
376
+ throw new Error(`package.json name [${pgkAppGroupName}] should be equal to pipeline var HEL_APP_GROUP_NAME [${HEL_APP_GROUP_NAME}]`);
377
+ }
378
+ }
379
379
  }
380
380
 
381
- /**
382
- *
383
- * @param {Array<string>} arr
384
- * @param {string} item
385
- */
386
- function noDupPush(arr, item) {
387
- if (!arr.includes(item)) arr.push(item);
381
+ /**
382
+ *
383
+ * @param {Array<string>} arr
384
+ * @param {string} item
385
+ */
386
+ function noDupPush(arr, item) {
387
+ if (!arr.includes(item)) arr.push(item);
388
388
  }
389
389
 
390
- function okeys(obj) {
391
- return Object.keys(obj);
390
+ function okeys(obj) {
391
+ return Object.keys(obj);
392
+ }
393
+
394
+ /**
395
+ *
396
+ * @param value
397
+ * @param {{nullValues: any[], isEmptyObjNull: boolean, isEmptyArrNull: boolean}} nullDef - 空定义
398
+ */
399
+ function isNull(value, nullDef = {}) {
400
+ const { nullValues = [null, undefined, ''], isEmptyObjNull = true, isEmptyArrNull = true } = nullDef;
401
+
402
+ const inNullValues = nullValues.includes(value);
403
+ if (inNullValues) {
404
+ return true;
405
+ }
406
+
407
+ if (Array.isArray(value)) {
408
+ if (isEmptyArrNull) return value.length === 0;
409
+ return false;
410
+ }
411
+
412
+ if (typeof value === 'object') {
413
+ if (isEmptyObjNull) return okeys(value).length === 0;
414
+ return false;
415
+ }
416
+
417
+ return false;
392
418
  }
393
419
 
394
- /**
395
- *
396
- * @param value
397
- * @param {{nullValues: any[], isEmptyObjNull: boolean, isEmptyArrNull: boolean}} nullDef - 空定义
398
- */
399
- function isNull(value, nullDef = {}) {
400
- const { nullValues = [null, undefined, ''], isEmptyObjNull = true, isEmptyArrNull = true } = nullDef;
401
-
402
- const inNullValues = nullValues.includes(value);
403
- if (inNullValues) {
404
- return true;
405
- }
406
-
407
- if (Array.isArray(value)) {
408
- if (isEmptyArrNull) return value.length === 0;
409
- return false;
410
- }
411
-
412
- if (typeof value === 'object') {
413
- if (isEmptyObjNull) return okeys(value).length === 0;
414
- return false;
415
- }
416
-
417
- return false;
420
+ /** @typedef {import('../../typings').SrcMap} SrcMap*/
421
+
422
+ /**
423
+ * @param {import('../../typings').IUserExtractOptions} extractOptions
424
+ */
425
+ function makeAppVersionSrcMap(extractOptions) {
426
+ const { appInfo, indexHtmlName = cst.DEFAULT_HTML_INDEX_NAME, extractMode = 'all' } = extractOptions;
427
+ const { homePage } = appInfo;
428
+ // 用于更新到数据库的app信息,通常来说在构建机器上触发
429
+ // 从上往下的key顺序也是在html创建的顺序
430
+ return {
431
+ webDirPath: homePage,
432
+ htmlIndexSrc: `${slash.end(homePage)}${indexHtmlName}`,
433
+ extractMode,
434
+ iframeSrc: '',
435
+ chunkCssSrcList: [], // all build css files
436
+ chunkJsSrcList: [], // all build js files
437
+ staticCssSrcList: [], // all static css files
438
+ staticJsSrcList: [], // all static js files
439
+ relativeCssSrcList: [], // all relative js files
440
+ relativeJsSrcList: [], // all relative js files
441
+ headAssetList: [],
442
+ bodyAssetList: [],
443
+ otherSrcList: [],
444
+ };
445
+ }
446
+
447
+ /**
448
+ * 从 index.html 提取资源的描述数据,包含 htmlContent、srcMap
449
+ * @param {import('../../typings').IUserExtractOptions} userExtractOptions
450
+ */
451
+ function makeArkMetaJson(userExtractOptions, parsedRet) {
452
+ const { packageJson, extractMode = 'build', subApp } = userExtractOptions;
453
+ const { homePage, groupName, name: appName, semverApi } = subApp;
454
+
455
+ /**
456
+ * 构建版本号,当指定了 homePage 且不想采用默认的版本号生成规则时,才需要透传 buildVer 值
457
+ * 默认生成规则:
458
+ * 内网包:裁出 homePage ${cdnHost}/${appZone}/${appName}_${dateStr} 里的 ${appName}_${dateStr} 作为版本号
459
+ * 外网包:pkg.version
460
+ */
461
+ let version = userExtractOptions.buildVer;
462
+ const packVer = packageJson.version;
463
+ if (!version) {
464
+ if (semverApi) {
465
+ version = packVer;
466
+ } else {
467
+ try {
468
+ // ${cdnHost}/${appZone}/${appName}_${dateStr}
469
+ const [, restStr] = homePage.split('//');
470
+ const [, , versionMakeOnPipeline] = restStr.split('/');
471
+ if (versionMakeOnPipeline) {
472
+ const arr = versionMakeOnPipeline.split('_');
473
+ const lastItem = arr[arr.length - 1];
474
+ // 特征符合 arkpack 的版本号
475
+ if (lastItem && lastItem.length === 14 && new RegExp('^[1-9]+[0-9]*$').test(lastItem)) {
476
+ version = versionMakeOnPipeline;
477
+ }
478
+ }
479
+ } catch (err) {}
480
+
481
+ if (!version) {
482
+ // 自定义 homePage 后,版本未必能推导出来,降级为使用 package.json 版本号
483
+ version = packVer;
484
+ }
485
+ }
486
+ }
487
+ const repo = packageJson.repository || {};
488
+
489
+ // 新增依赖处理逻辑
490
+ const { dependencies = {}, arkDependencies = {} } = packageJson;
491
+ const arklibDeps = Object.entries(dependencies)
492
+ .filter(([name]) => name.indexOf('arklib-') !== -1)
493
+ .reduce((acc, [name, version]) => ({ ...acc, [name]: version }), {});
494
+
495
+ return {
496
+ app: {
497
+ name: appName,
498
+ app_group_name: groupName,
499
+ git_repo_url: repo.url || packageJson.homepage || '',
500
+ online_version: version,
501
+ build_version: version,
502
+ },
503
+ version: {
504
+ plugin_ver: cst.PLUGIN_VER,
505
+ extract_mode: extractMode,
506
+ sub_app_name: appName,
507
+ sub_app_version: version,
508
+ // ... existing version fields ...
509
+ arkDependencies: { ...arklibDeps, ...arkDependencies },
510
+ src_map: parsedRet.srcMap,
511
+ html_content: parsedRet.htmlContent,
512
+ },
513
+ };
418
514
  }
419
515
 
420
- /** @typedef {import('../../typings').SrcMap} SrcMap*/
421
-
422
- /**
423
- * @param {import('../../typings').IUserExtractOptions} extractOptions
424
- */
425
- function makeAppVersionSrcMap(extractOptions) {
426
- const { appInfo, indexHtmlName = cst.DEFAULT_HTML_INDEX_NAME, extractMode = 'all' } = extractOptions;
427
- const { homePage } = appInfo;
428
- // 用于更新到数据库的app信息,通常来说在构建机器上触发
429
- // 从上往下的key顺序也是在html创建的顺序
430
- return {
431
- webDirPath: homePage,
432
- htmlIndexSrc: `${slash.end(homePage)}${indexHtmlName}`,
433
- extractMode,
434
- iframeSrc: '',
435
- chunkCssSrcList: [], // all build css files
436
- chunkJsSrcList: [], // all build js files
437
- staticCssSrcList: [], // all static css files
438
- staticJsSrcList: [], // all static js files
439
- relativeCssSrcList: [], // all relative js files
440
- relativeJsSrcList: [], // all relative js files
441
- headAssetList: [],
442
- bodyAssetList: [],
443
- otherSrcList: [],
444
- };
516
+ /** @typedef {import('../../typings').SrcMap} SrcMap*/
517
+
518
+ const writeFile = util__default["default"].promisify(fs__default["default"].writeFile);
519
+
520
+ /** jsdom 15 里去内联脚本 innerText 取不到,这里用此函数辅助 */
521
+ function getInnerText(/** @type {HTMLElement}} */ dom) {
522
+ return dom.innerText || dom.innerHTML || '';
523
+ }
524
+
525
+ function getDatasetVal(dataset, key, defaultValIfOnlyKey) {
526
+ const hasKey = Object.prototype.hasOwnProperty.call(dataset, key);
527
+ if (hasKey) {
528
+ return dataset[key] || defaultValIfOnlyKey;
529
+ }
530
+ return '';
531
+ }
532
+
533
+ function isRelativePath(path) {
534
+ if (path.startsWith('//')) return false;
535
+ return path.startsWith('/') || path.startsWith('./') || path.startsWith('../');
536
+ }
537
+
538
+ function buildAssetItem(tag, /** @type {IAssetInfo} */ assetInfo) {
539
+ const isLink = tag === 'link';
540
+ const { isBuildUrl, isNonBuildAndRelative, canAppend, el, url, innerText } = assetInfo;
541
+ let attrs = {};
542
+ if (url) {
543
+ attrs = isLink ? { href: url } : { src: url };
544
+ }
545
+
546
+ let tagVar = '';
547
+ if (isBuildUrl) {
548
+ tagVar = tag;
549
+ } else if (isNonBuildAndRelative) {
550
+ tagVar = isLink ? 'relativeLink' : 'relativeScript';
551
+ } else {
552
+ tagVar = isLink ? 'staticLink' : 'staticScript';
553
+ }
554
+
555
+ const assetItem = { tag: tagVar, append: canAppend, attrs };
556
+ if (innerText) {
557
+ assetItem.innerText = innerText;
558
+ }
559
+
560
+ const attrNames = el.getAttributeNames();
561
+ attrNames.forEach((name) => {
562
+ // src href 上面已记录真正的目标值,故移除
563
+ // data-helappend 只在提取元数据辅助计算 append 值时用到,故此处移除
564
+ if (['src', 'href', 'data-helappend'].includes(name)) return;
565
+ attrs[name] = el.getAttribute(name);
566
+ });
567
+
568
+ return assetItem;
569
+ }
570
+
571
+ // 注意:这个变量需要在每次编译时重置
572
+ // 不在这里维护,而是通过 options 传递
573
+ let custScriptIdx = 0;
574
+
575
+ function resetScriptIdx() {
576
+ custScriptIdx = 0;
577
+ }
578
+
579
+ /**
580
+ * @param {HTMLScriptElement | HTMLStyleElement} childDom
581
+ * @param {string} fileType
582
+ * @param {IInnerFillAssetListOptions} options
583
+ */
584
+ async function writeInnerText(childDom, fileType, options) {
585
+ const { homePage, buildDirFullPath, compilation, customAssets = [] } = options;
586
+ let innerText = getInnerText(childDom);
587
+ if (!innerText) return '';
588
+
589
+ verbose(`found a user customized ${fileType} tag node in html, try extract its content and write them to local fs`);
590
+ custScriptIdx += 1;
591
+ const scriptName = `ark_userChunk_${custScriptIdx}.${fileType}`;
592
+ const fileAbsolutePath = `${buildDirFullPath}/${scriptName}`;
593
+ const fileWebPath = `${slash.noEnd(homePage)}/${scriptName}`;
594
+
595
+ // 对于 JS 文件,移除或替换 document.write() 调用
596
+ // 因为异步加载的脚本中不能使用 document.write()
597
+ if (fileType === 'js') {
598
+ // 方法1:完全移除 document.write() 调用(推荐)
599
+ // innerText = innerText.replace(/document\.write\s*\([^)]*\)/g, '');
600
+
601
+ // 方法2:用 console.warn 替换 document.write(),保留代码逻辑但避免错误
602
+ innerText = innerText.replace(
603
+ /document\.write\s*\(([^)]*)\)/g,
604
+ 'console.warn("[ark-micro] document.write is not allowed in asynchronously loaded scripts:", $1)'
605
+ );
606
+ verbose(`Replaced document.write calls in ${scriptName}`);
607
+ }
608
+
609
+ // 在开发模式下,使用 compilation.emitAsset 确保文件能被 dev server 访问
610
+ if (compilation && compilation.emitAsset) {
611
+ const { sources } = compilation.compiler.webpack;
612
+ compilation.emitAsset(
613
+ scriptName,
614
+ new sources.RawSource(innerText)
615
+ );
616
+ verbose(`emit asset ${scriptName} via webpack compilation`);
617
+ } else {
618
+ // 生产模式或没有 compilation 对象时,直接写入文件系统
619
+ await writeFile(fileAbsolutePath, innerText);
620
+ }
621
+
622
+ // 将自定义资产信息保存到 customAssets 数组,供后续使用
623
+ if (customAssets) {
624
+ customAssets.push({ scriptName, fileWebPath, content: innerText });
625
+ }
626
+
627
+ verbose(`write done, the web file will be ${fileWebPath} later`);
628
+ return fileWebPath;
629
+ }
630
+
631
+ /**
632
+ * @param {string} url
633
+ * @param {IAssetOptions} options
634
+ */
635
+ function getAssetInfo(url, options) {
636
+ const { homePage, extractMode, enableRelativePath, enableAssetInnerText, el, innerText } = options;
637
+ const { dataset = {} } = el;
638
+ // 是构建生成的产物路径,无 url 的当做是内联的处理,isBuildUrl 强制为 true
639
+ const isBuildUrl = url ? url.startsWith(homePage) : true;
640
+ const isRelative = isRelativePath(url);
641
+ // 是 homePage 之外相对路径导入的产物路径
642
+ const isNonBuildAndRelative = !isBuildUrl && isRelative;
643
+ const isStatic = !isBuildUrl && !isRelative;
644
+ const isIcoAsset = url.endsWith('.ico');
645
+ // 设置了 extractMode 为 build 和 build_no_html 时,当前产物路径是非构建生成的,则直接忽略,不记录到 assetList 数据里
646
+ const ignoreAddToAssetList = !isBuildUrl && (extractMode === 'build' || extractMode === 'build_no_html');
647
+ let allowAddToAssetList = !ignoreAddToAssetList;
648
+ if (!allowAddToAssetList) {
649
+ verbose(` >>> ignore add asset [${url}] to assetList by extractMode=${extractMode}`);
650
+ }
651
+
652
+ // 未显式设置 data-helappend 时,helappend 默认值会走内部逻辑来决定如何赋值
653
+ let helAppendValOfDataset = getDatasetVal(dataset, 'helappend', '1');
654
+ let helAppendValOfInnerLogic = '';
655
+ if (helAppendValOfDataset && !['1', '0'].includes(helAppendValOfDataset)) {
656
+ throw new Error(`found invalid helappend value [${helAppendValOfDataset}], only accpet 1 or 0 currently`);
657
+ }
658
+
659
+ const ex = dataset.helex || '';
660
+ if (isNonBuildAndRelative) {
661
+ if (isIcoAsset && !helAppendValOfDataset) {
662
+ helAppendValOfDataset = '0'; // ico 文件特殊处理,默认是不加载的
663
+ } else if (ex && !helAppendValOfDataset) {
664
+ helAppendValOfDataset = '1'; // 标记了 ex 的文件特殊处理,默认需要加载,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
665
+ }
666
+
667
+ // 如下面错误描述所示,在既没有设置 enableRelativePath=true,又没有显式的标记 data-helappend 的情况下
668
+ // 不允许 homePage 之外的相对路径导入的资源存在
669
+ // 所以对于此类 homePage 之外的相对路径导入的资源,要么用户设置 enableRelativePath=true,要么显式的标记 data-helappend
670
+ // 设置 enableRelativePath=true 后,优先读可能已存在的 data-helappend 值,没有则默认为 0,表示不加载
671
+ // 不设置 enableRelativePath=true 的话,则需要用户一定标记 data-helappend 值
672
+ if (!enableRelativePath && !helAppendValOfDataset) {
673
+ throw new Error(
674
+ pfstr(`
675
+ found asset url [${url}] is a relative path, it is obviously not a valid url for cdn architecture deploy!
676
+ but if you are sure this url is valid, there are 2 ways to skip this error occured, you can choose any one of them:
677
+ 1. pass enableRelativePath true to ark-dev-utils.extractArkMetaJson method options.
678
+ 2. add data-helappend="0" on the asset dom attribute to tell ark-dev-utils ignore this asset.
679
+
680
+ ark-dev-utils will mark this url as relativeLink or relativeScript, and set append as false,
681
+ if you want sdk append this asset, you can explicitly add data-helappend="1" on the asset dom attribute.
682
+ a demo will be like:<script src="./a/b.js" data-helappend="1"></script>,<br/>
683
+ note that the asset will depend on your host site seriously under this situation.
684
+ `),
685
+ );
686
+ }
687
+
688
+ // 构建时设置 enableRelativePath=true,则允许此类【homePage之外相对路径导入的产物】加入到资源清单列表里
689
+ allowAddToAssetList = true;
690
+ helAppendValOfInnerLogic = helAppendValOfDataset || '0'; // 没有显示设定 data-helappend 时默认标记为不加载
691
+ } else if (isIcoAsset) {
692
+ helAppendValOfInnerLogic = '0'; // ico 文件特殊处理,默认是不加载的
693
+ } else if (isStatic) {
694
+ if (ex) {
695
+ // 对于标记了 helex 的元素,默认是 append 的,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
696
+ // 重复则不追加,不重复则追加
697
+ helAppendValOfInnerLogic = helAppendValOfDataset || '1';
698
+ } else {
699
+ helAppendValOfInnerLogic = '0';
700
+ }
701
+ } else {
702
+ // isBuild
703
+ helAppendValOfInnerLogic = '1'; // 标记为可加载
704
+ }
705
+
706
+ const helAppendVal = helAppendValOfDataset || helAppendValOfInnerLogic;
707
+ const helAppend = helAppendVal === '1';
708
+ if (ex && !helAppend) {
709
+ // 设置了 helex 但同时设置了不加载,会造成歧义,当前版本是不允许的
710
+ throw new Error(
711
+ pfstr(`
712
+ found conflict setting for helex: [data-helex="${ex}"]、[data-helappend="${helAppendVal}"],
713
+ remove data-helappend( append is true for helex by default ) or set data-helappend="1"!
714
+ `),
715
+ );
716
+ }
717
+
718
+ return {
719
+ url,
720
+ el,
721
+ isBuildUrl,
722
+ isNonBuildAndRelative,
723
+ canAppend: helAppend,
724
+ allowAddToAssetList,
725
+ innerText: enableAssetInnerText ? innerText : '',
726
+ };
727
+ }
728
+
729
+ /**
730
+ * 提取link、script标签数据并填充到目标assetList
731
+ * @param {HTMLCollectionOf<HTMLScriptElement>} doms
732
+ * @param {IInnerFillAssetListOptions} options
733
+ */
734
+ async function fillAssetList(doms, options) {
735
+ const {
736
+ homePage,
737
+ enableReplaceDevJs = true,
738
+ enableRelativePath = false,
739
+ enableAssetInnerText = false,
740
+ enablePrefixHomePage = false,
741
+ srcMap,
742
+ isHead,
743
+ } = options;
744
+ const {
745
+ headAssetList,
746
+ bodyAssetList,
747
+ extractMode,
748
+ chunkCssSrcList,
749
+ staticCssSrcList,
750
+ relativeCssSrcList,
751
+ chunkJsSrcList,
752
+ staticJsSrcList,
753
+ relativeJsSrcList,
754
+ } = srcMap;
755
+ const assetList = isHead ? headAssetList : bodyAssetList;
756
+
757
+ const len = doms.length;
758
+ const replaceContentList = [];
759
+ verbose(`extractMode is ${extractMode}`);
760
+ verbose(`enableAssetInnerText is ${enableAssetInnerText}`);
761
+
762
+ const pushToSrcList = (assetType, /** @type {IAssetInfo} */ assetInfo) => {
763
+ const { isBuildUrl, isNonBuildAndRelative, url } = assetInfo;
764
+ const isAllExtractMode = extractMode === 'all' || extractMode === 'all_no_html';
765
+
766
+ if (assetType === 'css') {
767
+ if (isBuildUrl) {
768
+ return noDupPush(chunkCssSrcList, url);
769
+ }
770
+ if (isAllExtractMode) {
771
+ const list = isNonBuildAndRelative ? relativeCssSrcList : staticCssSrcList;
772
+ return noDupPush(list, url);
773
+ }
774
+ }
775
+
776
+ if (isBuildUrl) {
777
+ return noDupPush(chunkJsSrcList, url);
778
+ }
779
+ if (isAllExtractMode) {
780
+ const list = isNonBuildAndRelative ? relativeJsSrcList : staticJsSrcList;
781
+ return noDupPush(list, url);
782
+ }
783
+ };
784
+
785
+ const mayPrefixHomePage = (url) => {
786
+ if (enablePrefixHomePage) {
787
+ if (url.startsWith('/') && !url.startsWith('//')) {
788
+ verbose(` >>> prefix homePage [${homePage}] for url [${url}]`);
789
+ const finalUrl = `${slash.noEnd(homePage)}${url}`;
790
+ replaceContentList.push({ toMatch: `="${url}"`, toReplace: `="${finalUrl}"` });
791
+ return finalUrl;
792
+ }
793
+ }
794
+ return url;
795
+ };
796
+
797
+ for (let i = 0; i < len; i++) {
798
+ const childDom = doms[i];
799
+ const { tagName, dataset = {} } = childDom;
800
+ const innerText = getInnerText(childDom);
801
+ let toPushAsset = null;
802
+ let allowAddToAssetList = false;
803
+ const assetOptions = { el: childDom, innerText, homePage, extractMode, enableRelativePath, enableAssetInnerText };
804
+
805
+ if (!['LINK', 'SCRIPT', 'STYLE'].includes(tagName)) {
806
+ continue;
807
+ }
808
+ if (!isNull(dataset)) {
809
+ verbose(`found ${tagName} dataset`, dataset);
810
+ }
811
+
812
+ if (tagName === 'LINK') {
813
+ const { hreflang = '', href: oriHref } = childDom;
814
+ verbose(`analyze link href:[${oriHref}]`);
815
+ if (!oriHref) continue;
816
+
817
+ let href = mayPrefixHomePage(oriHref);
818
+ verbose(`analyze link [${href}]`);
819
+ // 一些使用了老版本cra的项目,这两个href 在修改了 publicPath 后也不被添加前缀,这里做一下修正
820
+ const legacyHrefs = ['/manifest.json', '/favicon.ico'];
821
+ if (legacyHrefs.includes(href)) {
822
+ const oldHref = href;
823
+ href = `${homePage}${href}`;
824
+ replaceContentList.push({ toMatch: href, toReplace: href });
825
+ verbose(`replace link [${oldHref}] to [href]`);
826
+ }
827
+
828
+ const assetInfo = getAssetInfo(href, assetOptions);
829
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
830
+ // 供 shadow-dom 或其他需要知道当前应用所有样式列表的场景用
831
+ if (href.endsWith('.css')) {
832
+ pushToSrcList('css', assetInfo);
833
+ }
834
+ toPushAsset = buildAssetItem('link', assetInfo);
835
+ } else if (tagName === 'SCRIPT') {
836
+ const { src } = childDom;
837
+ verbose(`analyze script src:[${src}], innerText:[${innerText}]`);
838
+
839
+ if (!src && !innerText) {
840
+ continue;
841
+ }
842
+
843
+ if (!src && innerText && enableAssetInnerText) {
844
+ verbose('user set enableAssetInnerText = true and found SCRIPT node innerText');
845
+ const assetInfo = getAssetInfo('', assetOptions);
846
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
847
+ toPushAsset = buildAssetItem('script', assetInfo);
848
+ } else {
849
+ let targetSrc = src;
850
+ if (!targetSrc) {
851
+ targetSrc = await writeInnerText(childDom, 'js', options);
852
+ }
853
+ if (!targetSrc) continue;
854
+
855
+ targetSrc = mayPrefixHomePage(targetSrc);
856
+ if (enableReplaceDevJs) {
857
+ // 替换用户的 development 模式的 react 相关包体
858
+ if (targetSrc.endsWith('react.dev.js')) {
859
+ const toReplace = targetSrc.replace('react.dev.js', 'react.js');
860
+ replaceContentList.push({ toMatch: targetSrc, toReplace });
861
+ targetSrc = toReplace;
862
+ }
863
+ // 替换用户的 development 模式的 vue 相关包体
864
+ if (targetSrc.endsWith('vue.dev.js')) {
865
+ const toReplace = targetSrc.replace('vue.dev.js', 'vue.js');
866
+ replaceContentList.push({ toMatch: targetSrc, toReplace });
867
+ targetSrc = toReplace;
868
+ }
869
+ }
870
+
871
+ const assetInfo = getAssetInfo(targetSrc, assetOptions);
872
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
873
+
874
+ pushToSrcList('js', assetInfo);
875
+ toPushAsset = buildAssetItem('script', assetInfo);
876
+ }
877
+ } else if (tagName === 'STYLE') {
878
+ verbose(`analyze style innerText:[${innerText}]`);
879
+
880
+ if (innerText && enableAssetInnerText) {
881
+ verbose('user set enableAssetInnerText = true and found STYLE node innerText');
882
+ const assetInfo = getAssetInfo('', assetOptions);
883
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
884
+ toPushAsset = buildAssetItem('style', assetInfo);
885
+ } else {
886
+ // style 标签转换为 css 文件存起来,以便让 @arkxio/ark-micro 用 link 标签加载
887
+ let href = await writeInnerText(childDom, 'css', options);
888
+ if (!href) continue;
889
+
890
+ href = mayPrefixHomePage(href);
891
+ const assetInfo = getAssetInfo(href, assetOptions);
892
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
893
+
894
+ pushToSrcList('css', assetInfo);
895
+ toPushAsset = buildAssetItem('link', assetInfo);
896
+ }
897
+ }
898
+
899
+ if (toPushAsset && allowAddToAssetList) {
900
+ assetList.push(toPushAsset);
901
+ }
902
+ }
903
+
904
+ return replaceContentList;
445
905
  }
446
906
 
447
- /**
448
- * 从 index.html 提取资源的描述数据,包含 htmlContent、srcMap
449
- * @param {import('../../typings').IUserExtractOptions} userExtractOptions
450
- */
451
- function makeArkMetaJson(userExtractOptions, parsedRet) {
452
- const { packageJson, extractMode = 'build', subApp } = userExtractOptions;
453
- const { homePage, groupName, name: appName, semverApi } = subApp;
454
-
455
- /**
456
- * 构建版本号,当指定了 homePage 且不想采用默认的版本号生成规则时,才需要透传 buildVer
457
- * 默认生成规则:
458
- * 内网包:裁出 homePage ${cdnHost}/${appZone}/${appName}_${dateStr} 里的 ${appName}_${dateStr} 作为版本号
459
- * 外网包:pkg.version
460
- */
461
- let version = userExtractOptions.buildVer;
462
- const packVer = packageJson.version;
463
- if (!version) {
464
- if (semverApi) {
465
- version = packVer;
466
- } else {
467
- try {
468
- // ${cdnHost}/${appZone}/${appName}_${dateStr}
469
- const [, restStr] = homePage.split('//');
470
- const [, , versionMakeOnPipeline] = restStr.split('/');
471
- if (versionMakeOnPipeline) {
472
- const arr = versionMakeOnPipeline.split('_');
473
- const lastItem = arr[arr.length - 1];
474
- // 特征符合 arkpack 的版本号
475
- if (lastItem && lastItem.length === 14 && new RegExp('^[1-9]+[0-9]*$').test(lastItem)) {
476
- version = versionMakeOnPipeline;
477
- }
478
- }
479
- } catch (err) {}
480
-
481
- if (!version) {
482
- // 自定义 homePage 后,版本未必能推导出来,降级为使用 package.json 版本号
483
- version = packVer;
484
- }
485
- }
486
- }
487
- const repo = packageJson.repository || {};
488
-
489
- // 新增依赖处理逻辑
490
- const { dependencies = {}, arkDependencies = {} } = packageJson;
491
- const arklibDeps = Object.entries(dependencies)
492
- .filter(([name]) => name.indexOf('arklib-') !== -1)
493
- .reduce((acc, [name, version]) => ({ ...acc, [name]: version }), {});
494
-
495
- return {
496
- app: {
497
- name: appName,
498
- app_group_name: groupName,
499
- git_repo_url: repo.url || packageJson.homepage || '',
500
- online_version: version,
501
- build_version: version,
502
- },
503
- version: {
504
- plugin_ver: cst.PLUGIN_VER,
505
- extract_mode: extractMode,
506
- sub_app_name: appName,
507
- sub_app_version: version,
508
- // ... existing version fields ...
509
- arkDependencies: { ...arklibDeps, ...arkDependencies },
510
- src_map: parsedRet.srcMap,
511
- html_content: parsedRet.htmlContent,
512
- },
513
- };
907
+ /** @typedef {import('../../typings').SrcMap} SrcMap */
908
+
909
+ const readFile = util__namespace.promisify(fs__namespace.readFile);
910
+ const { JSDOM } = jsdom__default["default"];
911
+
912
+ /**
913
+ * @param {IUserExtractOptions} extractOptions
914
+ */
915
+ async function parseIndexHtml(extractOptions) {
916
+ const { appInfo, indexHtmlPath, extractMode = 'all', indexHtmlName = cst.DEFAULT_HTML_INDEX_NAME } = extractOptions;
917
+ const { name, homePage } = appInfo;
918
+ const htmlFilePath = `${indexHtmlPath}`;
919
+ verbose(`start to parse ${name} index.html file [${indexHtmlPath}]`);
920
+
921
+ let htmlContent = await readFile(htmlFilePath, { encoding: 'UTF-8' });
922
+ const srcMap = makeAppVersionSrcMap(extractOptions);
923
+
924
+ const dom = new JSDOM(htmlContent);
925
+ const { head, body } = dom.window.document;
926
+ /** @type {IInnerFillAssetListOptions} */
927
+ const fillAssetListOptions = { srcMap, homePage, ...extractOptions };
928
+ const [replaceContentListOfHead, replaceContentLisOfBody] = await Promise.all([
929
+ fillAssetList(head.children, { ...fillAssetListOptions, isHead: true }),
930
+ fillAssetList(body.children, fillAssetListOptions),
931
+ ]);
932
+
933
+ replaceContentListOfHead.forEach((item) => {
934
+ htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
935
+ });
936
+ replaceContentLisOfBody.forEach((item) => {
937
+ htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
938
+ });
939
+
940
+ const shouldRecordHtmlContent = extractMode === 'all' || extractMode === 'build';
941
+ const htmlContentVar = shouldRecordHtmlContent ? htmlContent : '';
942
+ if (!shouldRecordHtmlContent) {
943
+ verbose(`user set extractMode='${extractMode}', dev-utils will ignore write version.html_content`);
944
+ }
945
+
946
+ const hasReplacedContent = replaceContentListOfHead.length || replaceContentLisOfBody.length;
947
+ const parsedRet = { srcMap, htmlContent: htmlContentVar, hasReplacedContent };
948
+ verbose(`parse app [${name}] index.html file done!`);
949
+ verbose('replaceContentListOfHead: ', replaceContentListOfHead);
950
+ verbose('replaceContentLisOfBody: ', replaceContentLisOfBody);
951
+ verbose('parsedRet: ', parsedRet);
952
+ return parsedRet;
514
953
  }
515
954
 
516
- /** @typedef {import('../../typings').SrcMap} SrcMap*/
517
-
518
- const writeFile = util__default["default"].promisify(fs__default["default"].writeFile);
519
-
520
- /** jsdom 15 里去内联脚本 innerText 取不到,这里用此函数辅助 */
521
- function getInnerText(/** @type {HTMLElement}} */ dom) {
522
- return dom.innerText || dom.innerHTML || '';
955
+ /**
956
+ * 从 index.html 提取资源的描述数据,包含 htmlContent、srcMap
957
+ * @param {import('../../typings').IUserExtractOptions} userExtractOptions
958
+ */
959
+ async function extractArkMetaJson(userExtractOptions) {
960
+ const { buildDirFullPath, writeMetaJsonToDist = true } = userExtractOptions;
961
+ const appInfo = userExtractOptions.appInfo || userExtractOptions.subApp;
962
+ const indexHtmlName = userExtractOptions.indexHtmlName || 'index.html';// getIndexHtmlFileName(buildDirFullPath);
963
+
964
+ if (!appInfo) {
965
+ throw new Error('appInfo should be supplied in ver 3.0+ ark-dev-utils: extractArkMetaJson({appInfo, ...})');
966
+ }
967
+
968
+ const { homePage } = appInfo;
969
+ const options = { ...userExtractOptions, appInfo, subApp: appInfo, indexHtmlName };
970
+
971
+ verbose(`start extractArkMetaJson, appHomePage is [${homePage}]`);
972
+ // 分析 html 入口,提取 sdk 加载需要的资源清单
973
+ const parsedRet = await parseIndexHtml(options);
974
+ // 分析构建产物目录,提取剩余的资源清单补充到 chunkJsSrcList chunkCssSrcList 下,以便描述出应用的所有构建产物的资源路径
975
+ // fillAssetListByDist({ ...userExtractOptions, srcMap: parsedRet.srcMap, homePage, buildDirFullPath });
976
+ if (userExtractOptions.chunkJsFiles) {
977
+ for (const chunkJsFile of userExtractOptions.chunkJsFiles) {
978
+ noDupPush(parsedRet.srcMap.chunkJsSrcList, chunkJsFile);
979
+
980
+ noDupPush(parsedRet.srcMap.bodyAssetList, {
981
+ tag: 'script',
982
+ append: true,
983
+ attrs: {
984
+ src: chunkJsFile,
985
+ type: 'text/javascript',
986
+ },
987
+ });
988
+ }
989
+ }
990
+
991
+ // 有替换内容生成,则将 index.html 内容重写,让后续上传 cdn 步骤上传的是替换后的文件内容
992
+ if (parsedRet.hasReplacedContent) {
993
+ const htmlFilePath = `${buildDirFullPath}/${indexHtmlName}`;
994
+ fs__default["default"].writeFileSync(htmlFilePath, parsedRet.htmlContent, { encoding: 'utf-8' });
995
+ }
996
+
997
+ const arkMeta = makeArkMetaJson(options, parsedRet);
998
+ verbose('userExtractOptions.isDev', userExtractOptions.isDev);
999
+ if(userExtractOptions.isDev) {
1000
+ // 开发模式:写入内存文件系统
1001
+ const metaFilename = 'ark-meta.json';
1002
+ verbose('[begin] write memory ark meta ' + metaFilename);
1003
+
1004
+ const metaContent = JSON.stringify(arkMeta, null, 2);
1005
+ const compilation = userExtractOptions.compilation;
1006
+
1007
+ // Webpack 5 正确的 API:使用 emitAsset
1008
+ if (compilation.emitAsset) {
1009
+ const { sources } = compilation.compiler.webpack;
1010
+ compilation.emitAsset(
1011
+ metaFilename,
1012
+ new sources.RawSource(metaContent)
1013
+ );
1014
+ } else {
1015
+ // Webpack 4 兼容
1016
+ compilation.assets[metaFilename] = {
1017
+ source: () => metaContent,
1018
+ size: () => metaContent.length
1019
+ };
1020
+ }
1021
+ verbose('[finish] write memory ark meta ' + metaFilename);
1022
+ }else if (writeMetaJsonToDist) {
1023
+ // 生产模式:写入物理文件
1024
+ const arkMetaJsonFile = `${buildDirFullPath}/ark-meta.json`;
1025
+ verbose('finish write ark meta');
1026
+ fs__default["default"].writeFileSync(arkMetaJsonFile, JSON.stringify(arkMeta, null, 2));
1027
+ }
1028
+
1029
+ return arkMeta;
523
1030
  }
524
1031
 
525
- function getDatasetVal(dataset, key, defaultValIfOnlyKey) {
526
- const hasKey = Object.prototype.hasOwnProperty.call(dataset, key);
527
- if (hasKey) {
528
- return dataset[key] || defaultValIfOnlyKey;
529
- }
530
- return '';
531
- }
1032
+ var presetExternals = {
1033
+ react: {
1034
+ react: 'React',
1035
+ 'react-dom': 'ReactDOM',
1036
+ 'react-is': 'ReactIs',
1037
+ },
1038
+ /** 历史原因,暂留 vue2 */
1039
+ vue2: {
1040
+ vue: 'Vue',
1041
+ },
1042
+ /** 历史原因,暂留 vue3 */
1043
+ vue3: {
1044
+ vue: 'Vue',
1045
+ },
1046
+ vue: {
1047
+ vue: 'Vue',
1048
+ },
1049
+ };
532
1050
 
533
- function isRelativePath(path) {
534
- if (path.startsWith('//')) return false;
535
- return path.startsWith('/') || path.startsWith('./') || path.startsWith('../');
1051
+ /** @typedef {import('../../typings').IInnerSubAppOptions} IInnerSubAppOptions */
1052
+
1053
+ /**
1054
+ *
1055
+ * @param {Record<string, any>} pkg
1056
+ * @param {IInnerSubAppOptions} innerOptions
1057
+ * @param {ICreateSubAppOptions} [userOptions]
1058
+ * @returns
1059
+ */
1060
+ function createSubApp(pkg, innerOptions, userOptions) {
1061
+ const { frameworkType } = innerOptions;
1062
+ const optionsVar = Object.assign(
1063
+ {
1064
+ platform: cst.DEFAULT_PLAT,
1065
+ npmCdnType: cst.DEFAULT_NPM_CDN_TYPE,
1066
+ handleHomePage: true,
1067
+ semverApi: cst.DEFAULT_SEMVER_API,
1068
+ distDir: cst.ARK_DIST_DIR,
1069
+ },
1070
+ userOptions || {},
1071
+ );
1072
+ const envParams = getHelEnvParams(pkg, optionsVar);
1073
+ const externals = Object.assign({}, optionsVar.externals || {}, presetExternals[frameworkType] || {});
1074
+ const jsonpFnName = getJsonpFnName(envParams.appName || pkg.name);
1075
+
1076
+ return {
1077
+ platform: optionsVar.platform,
1078
+ /**
1079
+ * 资源的网络根目录
1080
+ * 形如:
1081
+ * 1 /web-app/sub-apps/ticket
1082
+ * 2 http://www.cdn.com/xxx/yyy
1083
+ */
1084
+ homePage: envParams.appHomePage,
1085
+ npmCdnType: optionsVar.npmCdnType,
1086
+ groupName: envParams.appGroupName,
1087
+ /** 构建时可注入到应用的APP_NAME下 */
1088
+ name: envParams.appName,
1089
+ externals,
1090
+ /**
1091
+ * @param {Record<string, any>} userExternals
1092
+ * @returns
1093
+ */
1094
+ getExternals: (userExternals) => {
1095
+ if (userExternals && !Array.isArray(userExternals)) {
1096
+ return { ...userExternals, externals };
1097
+ }
1098
+ return externals;
1099
+ },
1100
+ jsonpFnName,
1101
+ /**
1102
+ * @param {string} [fallbackPathOrUrl] 兜底用的 publicPathOrUrl
1103
+ * @param {boolean} [needEndSlash]
1104
+ * @returns
1105
+ */
1106
+ getPublicPathOrUrl: (fallbackPathOrUrl = '/', needEndSlash = true) => {
1107
+ let pathOrUrl = envParams.appHomePage;
1108
+ // 用户传递了非 / 的值时,优先采用用户传递的值
1109
+ if (pathOrUrl === '/' && fallbackPathOrUrl !== '/') {
1110
+ pathOrUrl = fallbackPathOrUrl;
1111
+ }
1112
+
1113
+ const finalPathOrUrl = getPublicPathOrUrl(pathOrUrl, needEndSlash);
1114
+ return finalPathOrUrl;
1115
+ },
1116
+ distDir: optionsVar.distDir,
1117
+ };
536
1118
  }
537
1119
 
538
- function buildAssetItem(tag, /** @type {IAssetInfo} */ assetInfo) {
539
- const isLink = tag === 'link';
540
- const { isBuildUrl, isNonBuildAndRelative, canAppend, el, url, innerText } = assetInfo;
541
- let attrs = {};
542
- if (url) {
543
- attrs = isLink ? { href: url } : { src: url };
544
- }
545
-
546
- let tagVar = '';
547
- if (isBuildUrl) {
548
- tagVar = tag;
549
- } else if (isNonBuildAndRelative) {
550
- tagVar = isLink ? 'relativeLink' : 'relativeScript';
551
- } else {
552
- tagVar = isLink ? 'staticLink' : 'staticScript';
553
- }
554
-
555
- const assetItem = { tag: tagVar, append: canAppend, attrs };
556
- if (innerText) {
557
- assetItem.innerText = innerText;
558
- }
559
-
560
- const attrNames = el.getAttributeNames();
561
- attrNames.forEach((name) => {
562
- // src href 上面已记录真正的目标值,故移除
563
- // data-helappend 只在提取元数据辅助计算 append 值时用到,故此处移除
564
- if (['src', 'href', 'data-helappend'].includes(name)) return;
565
- attrs[name] = el.getAttribute(name);
566
- });
567
-
568
- return assetItem;
1120
+ /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1121
+
1122
+ /**
1123
+ *
1124
+ * @param {Record<string, any>} pkg
1125
+ * @param {ICreateSubAppOptions} [options]
1126
+ * @returns
1127
+ */
1128
+ function createLibSubApp(pkg, options) {
1129
+ return createSubApp(pkg, { frameworkType: 'lib' }, options);
569
1130
  }
570
1131
 
571
- // 注意:这个变量需要在每次编译时重置
572
- // 不在这里维护,而是通过 options 传递
573
- let custScriptIdx = 0;
574
-
575
- function resetScriptIdx() {
576
- custScriptIdx = 0;
1132
+ /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1133
+
1134
+ /**
1135
+ *
1136
+ * @param {Record<string, any>} pkg
1137
+ * @param {ICreateSubAppOptions} [options]
1138
+ * @returns
1139
+ */
1140
+ function createReactSubApp(pkg, options) {
1141
+ return createSubApp(pkg, { frameworkType: 'react' }, options);
577
1142
  }
578
1143
 
579
- /**
580
- * @param {HTMLScriptElement | HTMLStyleElement} childDom
581
- * @param {string} fileType
582
- * @param {IInnerFillAssetListOptions} options
583
- */
584
- async function writeInnerText(childDom, fileType, options) {
585
- const { homePage, buildDirFullPath, compilation, customAssets = [] } = options;
586
- let innerText = getInnerText(childDom);
587
- if (!innerText) return '';
588
-
589
- verbose(`found a user customized ${fileType} tag node in html, try extract its content and write them to local fs`);
590
- custScriptIdx += 1;
591
- const scriptName = `ark_userChunk_${custScriptIdx}.${fileType}`;
592
- const fileAbsolutePath = `${buildDirFullPath}/${scriptName}`;
593
- const fileWebPath = `${slash.noEnd(homePage)}/${scriptName}`;
594
-
595
- // 对于 JS 文件,移除或替换 document.write() 调用
596
- // 因为异步加载的脚本中不能使用 document.write()
597
- if (fileType === 'js') {
598
- // 方法1:完全移除 document.write() 调用(推荐)
599
- // innerText = innerText.replace(/document\.write\s*\([^)]*\)/g, '');
600
-
601
- // 方法2:用 console.warn 替换 document.write(),保留代码逻辑但避免错误
602
- innerText = innerText.replace(
603
- /document\.write\s*\(([^)]*)\)/g,
604
- 'console.warn("[ark-micro] document.write is not allowed in asynchronously loaded scripts:", $1)'
605
- );
606
- verbose(`Replaced document.write calls in ${scriptName}`);
607
- }
608
-
609
- // 在开发模式下,使用 compilation.emitAsset 确保文件能被 dev server 访问
610
- if (compilation && compilation.emitAsset) {
611
- const { sources } = compilation.compiler.webpack;
612
- compilation.emitAsset(
613
- scriptName,
614
- new sources.RawSource(innerText)
615
- );
616
- verbose(`emit asset ${scriptName} via webpack compilation`);
617
- } else {
618
- // 生产模式或没有 compilation 对象时,直接写入文件系统
619
- await writeFile(fileAbsolutePath, innerText);
620
- }
621
-
622
- // 将自定义资产信息保存到 customAssets 数组,供后续使用
623
- if (customAssets) {
624
- customAssets.push({ scriptName, fileWebPath, content: innerText });
625
- }
626
-
627
- verbose(`write done, the web file will be ${fileWebPath} later`);
628
- return fileWebPath;
1144
+ /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1145
+
1146
+ /**
1147
+ * 创建 vue 应用的描述对象
1148
+ * @param {Record<string, any>} pkg
1149
+ * @param {ICreateSubAppOptions} [options]
1150
+ * @returns
1151
+ */
1152
+ function createVueSubApp(pkg, options) {
1153
+ return createSubApp(pkg, { frameworkType: 'vue' }, options);
629
1154
  }
630
1155
 
631
- /**
632
- * @param {string} url
633
- * @param {IAssetOptions} options
634
- */
635
- function getAssetInfo(url, options) {
636
- const { homePage, extractMode, enableRelativePath, enableAssetInnerText, el, innerText } = options;
637
- const { dataset = {} } = el;
638
- // 是构建生成的产物路径,无 url 的当做是内联的处理,isBuildUrl 强制为 true
639
- const isBuildUrl = url ? url.startsWith(homePage) : true;
640
- const isRelative = isRelativePath(url);
641
- // 是 homePage 之外相对路径导入的产物路径
642
- const isNonBuildAndRelative = !isBuildUrl && isRelative;
643
- const isStatic = !isBuildUrl && !isRelative;
644
- const isIcoAsset = url.endsWith('.ico');
645
- // 设置了 extractMode 为 build 和 build_no_html 时,当前产物路径是非构建生成的,则直接忽略,不记录到 assetList 数据里
646
- const ignoreAddToAssetList = !isBuildUrl && (extractMode === 'build' || extractMode === 'build_no_html');
647
- let allowAddToAssetList = !ignoreAddToAssetList;
648
- if (!allowAddToAssetList) {
649
- verbose(` >>> ignore add asset [${url}] to assetList by extractMode=${extractMode}`);
650
- }
651
-
652
- // 未显式设置 data-helappend 时,helappend 默认值会走内部逻辑来决定如何赋值
653
- let helAppendValOfDataset = getDatasetVal(dataset, 'helappend', '1');
654
- let helAppendValOfInnerLogic = '';
655
- if (helAppendValOfDataset && !['1', '0'].includes(helAppendValOfDataset)) {
656
- throw new Error(`found invalid helappend value [${helAppendValOfDataset}], only accpet 1 or 0 currently`);
657
- }
658
-
659
- const ex = dataset.helex || '';
660
- if (isNonBuildAndRelative) {
661
- if (isIcoAsset && !helAppendValOfDataset) {
662
- helAppendValOfDataset = '0'; // ico 文件特殊处理,默认是不加载的
663
- } else if (ex && !helAppendValOfDataset) {
664
- helAppendValOfDataset = '1'; // 标记了 ex 的文件特殊处理,默认需要加载,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
665
- }
666
-
667
- // 如下面错误描述所示,在既没有设置 enableRelativePath=true,又没有显式的标记 data-helappend 的情况下
668
- // 不允许 homePage 之外的相对路径导入的资源存在
669
- // 所以对于此类 homePage 之外的相对路径导入的资源,要么用户设置 enableRelativePath=true,要么显式的标记 data-helappend
670
- // 设置 enableRelativePath=true 后,优先读可能已存在的 data-helappend 值,没有则默认为 0,表示不加载
671
- // 不设置 enableRelativePath=true 的话,则需要用户一定标记 data-helappend 值
672
- if (!enableRelativePath && !helAppendValOfDataset) {
673
- throw new Error(
674
- pfstr(`
675
- found asset url [${url}] is a relative path, it is obviously not a valid url for cdn architecture deploy!
676
- but if you are sure this url is valid, there are 2 ways to skip this error occured, you can choose any one of them:
677
- 1. pass enableRelativePath true to ark-dev-utils.extractArkMetaJson method options.
678
- 2. add data-helappend="0" on the asset dom attribute to tell ark-dev-utils ignore this asset.
679
-
680
- ark-dev-utils will mark this url as relativeLink or relativeScript, and set append as false,
681
- if you want sdk append this asset, you can explicitly add data-helappend="1" on the asset dom attribute.
682
- a demo will be like:<script src="./a/b.js" data-helappend="1"></script>,<br/>
683
- note that the asset will depend on your host site seriously under this situation.
684
- `),
685
- );
686
- }
1156
+ var ExternalsDefault = {
1157
+
1158
+ vue: 'Vue',
1159
+ axios: 'axios',
1160
+ // vuex: 'Vuex',
1161
+ // 'vue-dim'
1162
+ pinia: 'Pinia',
1163
+ 'pinia-plugin-persistedstate': 'piniaPluginPersistedstate',
1164
+ 'vue-i18n': 'VueI18n',
1165
+ 'vue-router': 'VueRouter',
1166
+ 'element-plus': 'ElementPlus',
1167
+ '@arkxio/ark-micro-core': 'ArkMicroCore',
1168
+ '@arkxio/ark-lib-proxy': 'ArkLibProxy',
1169
+ '@arkxio/ark-micro': 'ArkMicro',
1170
+ '@arkxio/ark-plugin': 'ArkPlugin'
1171
+ // 'echarts': 'echarts',
1172
+ // 'tinymce': 'tinymce'
1173
+ // "loadjs": { //将vue依赖 "外部化",不打包进组件库
1174
+ // root: 'loadjs',
1175
+ // commonjs: 'loadjs',
1176
+ // commonjs2: 'loadjs',
1177
+ // amd: 'loadjs'
1178
+ // }
1179
+
1180
+ };
687
1181
 
688
- // 构建时设置 enableRelativePath=true,则允许此类【homePage之外相对路径导入的产物】加入到资源清单列表里
689
- allowAddToAssetList = true;
690
- helAppendValOfInnerLogic = helAppendValOfDataset || '0'; // 没有显示设定 data-helappend 时默认标记为不加载
691
- } else if (isIcoAsset) {
692
- helAppendValOfInnerLogic = '0'; // ico 文件特殊处理,默认是不加载的
693
- } else if (isStatic) {
694
- if (ex) {
695
- // 对于标记了 helex 的元素,默认是 append 的,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
696
- // 重复则不追加,不重复则追加
697
- helAppendValOfInnerLogic = helAppendValOfDataset || '1';
698
- } else {
699
- helAppendValOfInnerLogic = '0';
700
- }
701
- } else {
702
- // isBuild
703
- helAppendValOfInnerLogic = '1'; // 标记为可加载
704
- }
1182
+ class ArkWebpackPlugin {
705
1183
 
706
- const helAppendVal = helAppendValOfDataset || helAppendValOfInnerLogic;
707
- const helAppend = helAppendVal === '1';
708
- if (ex && !helAppend) {
709
- // 设置了 helex 但同时设置了不加载,会造成歧义,当前版本是不允许的
710
- throw new Error(
711
- pfstr(`
712
- found conflict setting for helex: [data-helex="${ex}"]、[data-helappend="${helAppendVal}"],
713
- remove data-helappend( append is true for helex by default ) or set data-helappend="1"!
714
- `),
715
- );
1184
+ constructor(options) {
1185
+ this.metaOptions = options;
716
1186
  }
717
1187
 
718
- return {
719
- url,
720
- el,
721
- isBuildUrl,
722
- isNonBuildAndRelative,
723
- canAppend: helAppend,
724
- allowAddToAssetList,
725
- innerText: enableAssetInnerText ? innerText : '',
726
- };
727
- }
728
-
729
- /**
730
- * 提取link、script标签数据并填充到目标assetList
731
- * @param {HTMLCollectionOf<HTMLScriptElement>} doms
732
- * @param {IInnerFillAssetListOptions} options
733
- */
734
- async function fillAssetList(doms, options) {
735
- const {
736
- homePage,
737
- enableReplaceDevJs = true,
738
- enableRelativePath = false,
739
- enableAssetInnerText = false,
740
- enablePrefixHomePage = false,
741
- srcMap,
742
- isHead,
743
- } = options;
744
- const {
745
- headAssetList,
746
- bodyAssetList,
747
- extractMode,
748
- chunkCssSrcList,
749
- staticCssSrcList,
750
- relativeCssSrcList,
751
- chunkJsSrcList,
752
- staticJsSrcList,
753
- relativeJsSrcList,
754
- } = srcMap;
755
- const assetList = isHead ? headAssetList : bodyAssetList;
756
-
757
- const len = doms.length;
758
- const replaceContentList = [];
759
- verbose(`extractMode is ${extractMode}`);
760
- verbose(`enableAssetInnerText is ${enableAssetInnerText}`);
761
-
762
- const pushToSrcList = (assetType, /** @type {IAssetInfo} */ assetInfo) => {
763
- const { isBuildUrl, isNonBuildAndRelative, url } = assetInfo;
764
- const isAllExtractMode = extractMode === 'all' || extractMode === 'all_no_html';
765
-
766
- if (assetType === 'css') {
767
- if (isBuildUrl) {
768
- return noDupPush(chunkCssSrcList, url);
769
- }
770
- if (isAllExtractMode) {
771
- const list = isNonBuildAndRelative ? relativeCssSrcList : staticCssSrcList;
772
- return noDupPush(list, url);
773
- }
774
- }
775
-
776
- if (isBuildUrl) {
777
- return noDupPush(chunkJsSrcList, url);
778
- }
779
- if (isAllExtractMode) {
780
- const list = isNonBuildAndRelative ? relativeJsSrcList : staticJsSrcList;
781
- return noDupPush(list, url);
782
- }
783
- };
784
-
785
- const mayPrefixHomePage = (url) => {
786
- if (enablePrefixHomePage) {
787
- if (url.startsWith('/') && !url.startsWith('//')) {
788
- verbose(` >>> prefix homePage [${homePage}] for url [${url}]`);
789
- const finalUrl = `${slash.noEnd(homePage)}${url}`;
790
- replaceContentList.push({ toMatch: `="${url}"`, toReplace: `="${finalUrl}"` });
791
- return finalUrl;
792
- }
793
- }
794
- return url;
795
- };
796
-
797
- for (let i = 0; i < len; i++) {
798
- const childDom = doms[i];
799
- const { tagName, dataset = {} } = childDom;
800
- const innerText = getInnerText(childDom);
801
- let toPushAsset = null;
802
- let allowAddToAssetList = false;
803
- const assetOptions = { el: childDom, innerText, homePage, extractMode, enableRelativePath, enableAssetInnerText };
804
-
805
- if (!['LINK', 'SCRIPT', 'STYLE'].includes(tagName)) {
806
- continue;
807
- }
808
- if (!isNull(dataset)) {
809
- verbose(`found ${tagName} dataset`, dataset);
810
- }
1188
+ apply(compiler) {
1189
+ // 获取项目根目录路径
1190
+ compiler.context;
1191
+ const useExtractOptions = this.metaOptions;
811
1192
 
812
- if (tagName === 'LINK') {
813
- const { hreflang = '', href: oriHref } = childDom;
814
- verbose(`analyze link href:[${oriHref}]`);
815
- if (!oriHref) continue;
816
-
817
- let href = mayPrefixHomePage(oriHref);
818
- verbose(`analyze link [${href}]`);
819
- // 一些使用了老版本cra的项目,这两个href 在修改了 publicPath 后也不被添加前缀,这里做一下修正
820
- const legacyHrefs = ['/manifest.json', '/favicon.ico'];
821
- if (legacyHrefs.includes(href)) {
822
- const oldHref = href;
823
- href = `${homePage}${href}`;
824
- replaceContentList.push({ toMatch: href, toReplace: href });
825
- verbose(`replace link [${oldHref}] to [href]`);
826
- }
827
-
828
- const assetInfo = getAssetInfo(href, assetOptions);
829
- allowAddToAssetList = assetInfo.allowAddToAssetList;
830
- // 供 shadow-dom 或其他需要知道当前应用所有样式列表的场景用
831
- if (href.endsWith('.css')) {
832
- pushToSrcList('css', assetInfo);
833
- }
834
- toPushAsset = buildAssetItem('link', assetInfo);
835
- } else if (tagName === 'SCRIPT') {
836
- const { src } = childDom;
837
- verbose(`analyze script src:[${src}], innerText:[${innerText}]`);
838
-
839
- if (!src && !innerText) {
840
- continue;
841
- }
1193
+ // 在每次编译开始时重置脚本索引计数器,确保文件名一致性
1194
+ compiler.hooks.thisCompilation.tap('ArkResetScriptIdx', () => {
1195
+ resetScriptIdx();
1196
+ });
842
1197
 
843
- if (!src && innerText && enableAssetInnerText) {
844
- verbose('user set enableAssetInnerText = true and found SCRIPT node innerText');
845
- const assetInfo = getAssetInfo('', assetOptions);
846
- allowAddToAssetList = assetInfo.allowAddToAssetList;
847
- toPushAsset = buildAssetItem('script', assetInfo);
848
- } else {
849
- let targetSrc = src;
850
- if (!targetSrc) {
851
- targetSrc = await writeInnerText(childDom, 'js', options);
1198
+ compiler.hooks.thisCompilation.tap('ArkDynamicCdnChunkLoaderPlugin', (compilation) => {
1199
+ compilation.hooks.processAssets.tap(
1200
+ {
1201
+ name: 'ArkDynamicCdnChunkLoaderPlugin',
1202
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
1203
+ },
1204
+ (assets) => {
1205
+ // 新增分块文件过滤逻辑
1206
+ const chunkFiles = new Set();
1207
+ compilation.chunks.forEach(chunk => {
1208
+ chunk.files.forEach(file => {
1209
+ if (file.endsWith('.js')) chunkFiles.add(file);
1210
+ });
1211
+ });
1212
+ // 调试日志
1213
+ verbose('[分块文件列表]', Array.from(chunkFiles));
1214
+
1215
+ Object.entries(assets).forEach(([filename, source]) => {
1216
+ // 仅处理分块JS文件
1217
+ if (!chunkFiles.has(filename)) return;
1218
+
1219
+ // 检查资源内容有效性
1220
+ if (!source || !source.source || typeof source.source() !== 'string' || source.source().trim() === '') {
1221
+ console.warn(`资源 ${filename} 内容为空或无效,跳过处理`);
1222
+ return;
1223
+ }
1224
+
1225
+ let content = source.source().toString();
1226
+
1227
+ // 替换逻辑
1228
+ // /(__webpack_require__\.l)\(url,/g)
1229
+ // const webpackRequireLPattern = /(__webpack_require__\.l)\(url,/g;
1230
+ // if (webpackRequireLPattern.test(content)) {
1231
+ // verbose('检测到 __webpack_require__.l 模式');
1232
+ // content = content.replace(
1233
+ // webpackRequireLPattern,
1234
+ // `(function(url, done, key, chunkId) {
1235
+ // if (typeof window.ArkConfig !== 'undefined' && window.ArkConfig.cdnHost) {
1236
+ // if (!url.startsWith(window.ArkConfig.cdnHost) && url.startsWith('https://unpkg.com')) {
1237
+ // url = window.ArkConfig.cdnHost + url.slice('https://unpkg.com'.length);
1238
+ // }
1239
+ // }
1240
+ // return $1(url, done, key, chunkId);
1241
+ // })(url,`
1242
+ // );
1243
+ // const webpackRequireLPattern = /"(https:\/\/unpkg\.com)/g;
1244
+ // 新:捕获前面可选的 \(eval 模式下会有 \")
1245
+ const webpackRequireLPattern = /(\\?")https:\/\/unpkg\.com/g;
1246
+ if (webpackRequireLPattern.test(content)) {
1247
+ verbose('检测到 unpkg.com 模式');
1248
+ content = content.replace(webpackRequireLPattern, `window.ArkConfig.cdnHost + "`);
1249
+
1250
+ // 更新资源
1251
+ compilation.updateAsset(filename, new compiler.webpack.sources.RawSource(content));
1252
+ }
1253
+ });
852
1254
  }
853
- if (!targetSrc) continue;
1255
+ );
1256
+ });
854
1257
 
855
- targetSrc = mayPrefixHomePage(targetSrc);
856
- if (enableReplaceDevJs) {
857
- // 替换用户的 development 模式的 react 相关包体
858
- if (targetSrc.endsWith('react.dev.js')) {
859
- const toReplace = targetSrc.replace('react.dev.js', 'react.js');
860
- replaceContentList.push({ toMatch: targetSrc, toReplace });
861
- targetSrc = toReplace;
862
- }
863
- // 替换用户的 development 模式的 vue 相关包体
864
- if (targetSrc.endsWith('vue.dev.js')) {
865
- const toReplace = targetSrc.replace('vue.dev.js', 'vue.js');
866
- replaceContentList.push({ toMatch: targetSrc, toReplace });
867
- targetSrc = toReplace;
1258
+ compiler.hooks.emit.tapPromise('ArkMetaJsonGeneratorPlugin', async (compilation) => {
1259
+ // 注入环境判断脚本
1260
+ const isDev = process.env.NODE_ENV === 'development';
1261
+
1262
+ // 收集所有生成的JS文件
1263
+ const outputPath = compilation.options.output.publicPath || '';
1264
+ const jsFiles = [];
1265
+
1266
+ // Webpack 5 正确的API:使用 getAssets()
1267
+ const assets = compilation.getAssets ? compilation.getAssets() : compilation.assets;
1268
+
1269
+ // 遍历所有资产,收集JS文件
1270
+ assets.forEach((asset) => {
1271
+ const assetName = asset.name || asset;
1272
+ if (assetName.endsWith('.js')) {
1273
+ console.log('Found JS file:', assetName);
1274
+ // 排除 ark-meta.json 和 ark_userChunk 文件
1275
+ if (assetName.indexOf('js/app.') !== -1 ||
1276
+ assetName.indexOf('js/runtime.') !== -1 ||
1277
+ assetName.indexOf('js/vendors.') !== -1 ||
1278
+ assetName.indexOf('js/main.') !== -1) {
1279
+ jsFiles.push(outputPath + assetName);
868
1280
  }
869
1281
  }
870
-
871
- const assetInfo = getAssetInfo(targetSrc, assetOptions);
872
- allowAddToAssetList = assetInfo.allowAddToAssetList;
873
-
874
- pushToSrcList('js', assetInfo);
875
- toPushAsset = buildAssetItem('script', assetInfo);
876
- }
877
- } else if (tagName === 'STYLE') {
878
- verbose(`analyze style innerText:[${innerText}]`);
879
-
880
- if (innerText && enableAssetInnerText) {
881
- verbose('user set enableAssetInnerText = true and found STYLE node innerText');
882
- const assetInfo = getAssetInfo('', assetOptions);
883
- allowAddToAssetList = assetInfo.allowAddToAssetList;
884
- toPushAsset = buildAssetItem('style', assetInfo);
885
- } else {
886
- // style 标签转换为 css 文件存起来,以便让 @arkxio/ark-micro 用 link 标签加载
887
- let href = await writeInnerText(childDom, 'css', options);
888
- if (!href) continue;
889
-
890
- href = mayPrefixHomePage(href);
891
- const assetInfo = getAssetInfo(href, assetOptions);
892
- allowAddToAssetList = assetInfo.allowAddToAssetList;
893
-
894
- pushToSrcList('css', assetInfo);
895
- toPushAsset = buildAssetItem('link', assetInfo);
896
- }
897
- }
898
-
899
- if (toPushAsset && allowAddToAssetList) {
900
- assetList.push(toPushAsset);
901
- }
902
- }
903
-
904
- return replaceContentList;
905
- }
906
-
907
- /** @typedef {import('../../typings').SrcMap} SrcMap */
908
-
909
- const readFile = util__namespace.promisify(fs__namespace.readFile);
910
- const { JSDOM } = jsdom__default["default"];
911
-
912
- /**
913
- * @param {IUserExtractOptions} extractOptions
914
- */
915
- async function parseIndexHtml(extractOptions) {
916
- const { appInfo, indexHtmlPath, extractMode = 'all', indexHtmlName = cst.DEFAULT_HTML_INDEX_NAME } = extractOptions;
917
- const { name, homePage } = appInfo;
918
- const htmlFilePath = `${indexHtmlPath}`;
919
- verbose(`start to parse ${name} index.html file [${indexHtmlPath}]`);
920
-
921
- let htmlContent = await readFile(htmlFilePath, { encoding: 'UTF-8' });
922
- const srcMap = makeAppVersionSrcMap(extractOptions);
923
-
924
- const dom = new JSDOM(htmlContent);
925
- const { head, body } = dom.window.document;
926
- /** @type {IInnerFillAssetListOptions} */
927
- const fillAssetListOptions = { srcMap, homePage, ...extractOptions };
928
- const [replaceContentListOfHead, replaceContentLisOfBody] = await Promise.all([
929
- fillAssetList(head.children, { ...fillAssetListOptions, isHead: true }),
930
- fillAssetList(body.children, fillAssetListOptions),
931
- ]);
932
-
933
- replaceContentListOfHead.forEach((item) => {
934
- htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
935
- });
936
- replaceContentLisOfBody.forEach((item) => {
937
- htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
938
- });
939
-
940
- const shouldRecordHtmlContent = extractMode === 'all' || extractMode === 'build';
941
- const htmlContentVar = shouldRecordHtmlContent ? htmlContent : '';
942
- if (!shouldRecordHtmlContent) {
943
- verbose(`user set extractMode='${extractMode}', dev-utils will ignore write version.html_content`);
944
- }
945
-
946
- const hasReplacedContent = replaceContentListOfHead.length || replaceContentLisOfBody.length;
947
- const parsedRet = { srcMap, htmlContent: htmlContentVar, hasReplacedContent };
948
- verbose(`parse app [${name}] index.html file done!`);
949
- verbose('replaceContentListOfHead: ', replaceContentListOfHead);
950
- verbose('replaceContentLisOfBody: ', replaceContentLisOfBody);
951
- verbose('parsedRet: ', parsedRet);
952
- return parsedRet;
953
- }
954
-
955
- /**
956
- * 从 index.html 提取资源的描述数据,包含 htmlContent、srcMap
957
- * @param {import('../../typings').IUserExtractOptions} userExtractOptions
958
- */
959
- async function extractArkMetaJson(userExtractOptions) {
960
- const { buildDirFullPath, writeMetaJsonToDist = true } = userExtractOptions;
961
- const appInfo = userExtractOptions.appInfo || userExtractOptions.subApp;
962
- const indexHtmlName = userExtractOptions.indexHtmlName || 'index.html';// getIndexHtmlFileName(buildDirFullPath);
963
-
964
- if (!appInfo) {
965
- throw new Error('appInfo should be supplied in ver 3.0+ ark-dev-utils: extractArkMetaJson({appInfo, ...})');
966
- }
967
-
968
- const { homePage } = appInfo;
969
- const options = { ...userExtractOptions, appInfo, subApp: appInfo, indexHtmlName };
970
-
971
- verbose(`start extractArkMetaJson, appHomePage is [${homePage}]`);
972
- // 分析 html 入口,提取 sdk 加载需要的资源清单
973
- const parsedRet = await parseIndexHtml(options);
974
- // 分析构建产物目录,提取剩余的资源清单补充到 chunkJsSrcList chunkCssSrcList 下,以便描述出应用的所有构建产物的资源路径
975
- // fillAssetListByDist({ ...userExtractOptions, srcMap: parsedRet.srcMap, homePage, buildDirFullPath });
976
- if (userExtractOptions.chunkJsFiles) {
977
- for (const chunkJsFile of userExtractOptions.chunkJsFiles) {
978
- noDupPush(parsedRet.srcMap.chunkJsSrcList, chunkJsFile);
979
-
980
- noDupPush(parsedRet.srcMap.bodyAssetList, {
981
- tag: 'script',
982
- append: true,
983
- attrs: {
984
- src: chunkJsFile,
985
- type: 'text/javascript',
986
- },
987
1282
  });
988
- }
989
- }
990
1283
 
991
- // 有替换内容生成,则将 index.html 内容重写,让后续上传 cdn 步骤上传的是替换后的文件内容
992
- if (parsedRet.hasReplacedContent) {
993
- const htmlFilePath = `${buildDirFullPath}/${indexHtmlName}`;
994
- fs__default["default"].writeFileSync(htmlFilePath, parsedRet.htmlContent, { encoding: 'utf-8' });
995
- }
1284
+ console.log('Final JS files for chunkJsSrcList:', jsFiles);
996
1285
 
997
- const arkMeta = makeArkMetaJson(options, parsedRet);
998
- verbose('userExtractOptions.isDev', userExtractOptions.isDev);
999
- if(userExtractOptions.isDev) {
1000
- // 开发模式:写入内存文件系统
1001
- const metaFilename = 'ark-meta.json';
1002
- verbose('[begin] write memory ark meta ' + metaFilename);
1286
+ // 创建 customAssets 数组用于收集自定义资产(如从 HTML 内联 script 提取的代码)
1287
+ const customAssets = [];
1003
1288
 
1004
- const metaContent = JSON.stringify(arkMeta, null, 2);
1005
- const compilation = userExtractOptions.compilation;
1289
+ await extractArkMetaJson({
1290
+ ...useExtractOptions,
1291
+ chunkJsFiles: jsFiles,
1292
+ isDev: isDev,
1293
+ compilation: compilation,
1294
+ customAssets: customAssets
1295
+ });
1006
1296
 
1007
- // Webpack 5 正确的 API:使用 emitAsset
1008
- if (compilation.emitAsset) {
1009
- const { sources } = compilation.compiler.webpack;
1010
- compilation.emitAsset(
1011
- metaFilename,
1012
- new sources.RawSource(metaContent)
1013
- );
1014
- } else {
1015
- // Webpack 4 兼容
1016
- compilation.assets[metaFilename] = {
1017
- source: () => metaContent,
1018
- size: () => metaContent.length
1019
- };
1020
- }
1021
- verbose('[finish] write memory ark meta ' + metaFilename);
1022
- }else if (writeMetaJsonToDist) {
1023
- // 生产模式:写入物理文件
1024
- const arkMetaJsonFile = `${buildDirFullPath}/ark-meta.json`;
1025
- verbose('finish write ark meta');
1026
- fs__default["default"].writeFileSync(arkMetaJsonFile, JSON.stringify(arkMeta, null, 2));
1297
+ return Promise.resolve();
1298
+ });
1027
1299
  }
1028
-
1029
- return arkMeta;
1030
- }
1031
-
1032
- var presetExternals = {
1033
- react: {
1034
- react: 'React',
1035
- 'react-dom': 'ReactDOM',
1036
- 'react-is': 'ReactIs',
1037
- },
1038
- /** 历史原因,暂留 vue2 */
1039
- vue2: {
1040
- vue: 'Vue',
1041
- },
1042
- /** 历史原因,暂留 vue3 */
1043
- vue3: {
1044
- vue: 'Vue',
1045
- },
1046
- vue: {
1047
- vue: 'Vue',
1048
- },
1049
- };
1050
-
1051
- /** @typedef {import('../../typings').IInnerSubAppOptions} IInnerSubAppOptions */
1052
-
1053
- /**
1054
- *
1055
- * @param {Record<string, any>} pkg
1056
- * @param {IInnerSubAppOptions} innerOptions
1057
- * @param {ICreateSubAppOptions} [userOptions]
1058
- * @returns
1059
- */
1060
- function createSubApp(pkg, innerOptions, userOptions) {
1061
- const { frameworkType } = innerOptions;
1062
- const optionsVar = Object.assign(
1063
- {
1064
- platform: cst.DEFAULT_PLAT,
1065
- npmCdnType: cst.DEFAULT_NPM_CDN_TYPE,
1066
- handleHomePage: true,
1067
- semverApi: cst.DEFAULT_SEMVER_API,
1068
- distDir: cst.ARK_DIST_DIR,
1069
- },
1070
- userOptions || {},
1071
- );
1072
- const envParams = getHelEnvParams(pkg, optionsVar);
1073
- const externals = Object.assign({}, optionsVar.externals || {}, presetExternals[frameworkType] || {});
1074
- const jsonpFnName = getJsonpFnName(envParams.appName || pkg.name);
1075
-
1076
- return {
1077
- platform: optionsVar.platform,
1078
- /**
1079
- * 资源的网络根目录
1080
- * 形如:
1081
- * 1 /web-app/sub-apps/ticket
1082
- * 2 http://www.cdn.com/xxx/yyy
1083
- */
1084
- homePage: envParams.appHomePage,
1085
- npmCdnType: optionsVar.npmCdnType,
1086
- groupName: envParams.appGroupName,
1087
- /** 构建时可注入到应用的APP_NAME下 */
1088
- name: envParams.appName,
1089
- externals,
1090
- /**
1091
- * @param {Record<string, any>} userExternals
1092
- * @returns
1093
- */
1094
- getExternals: (userExternals) => {
1095
- if (userExternals && !Array.isArray(userExternals)) {
1096
- return { ...userExternals, externals };
1097
- }
1098
- return externals;
1099
- },
1100
- jsonpFnName,
1101
- /**
1102
- * @param {string} [fallbackPathOrUrl] 兜底用的 publicPathOrUrl
1103
- * @param {boolean} [needEndSlash]
1104
- * @returns
1105
- */
1106
- getPublicPathOrUrl: (fallbackPathOrUrl = '/', needEndSlash = true) => {
1107
- let pathOrUrl = envParams.appHomePage;
1108
- // 用户传递了非 / 的值时,优先采用用户传递的值
1109
- if (pathOrUrl === '/' && fallbackPathOrUrl !== '/') {
1110
- pathOrUrl = fallbackPathOrUrl;
1111
- }
1112
-
1113
- const finalPathOrUrl = getPublicPathOrUrl(pathOrUrl, needEndSlash);
1114
- return finalPathOrUrl;
1115
- },
1116
- distDir: optionsVar.distDir,
1117
- };
1118
- }
1119
-
1120
- /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1121
-
1122
- /**
1123
- *
1124
- * @param {Record<string, any>} pkg
1125
- * @param {ICreateSubAppOptions} [options]
1126
- * @returns
1127
- */
1128
- function createLibSubApp(pkg, options) {
1129
- return createSubApp(pkg, { frameworkType: 'lib' }, options);
1130
- }
1131
-
1132
- /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1133
-
1134
- /**
1135
- *
1136
- * @param {Record<string, any>} pkg
1137
- * @param {ICreateSubAppOptions} [options]
1138
- * @returns
1139
- */
1140
- function createReactSubApp(pkg, options) {
1141
- return createSubApp(pkg, { frameworkType: 'react' }, options);
1142
- }
1143
-
1144
- /** @typedef {import('../../typings').ICreateSubAppOptions} ICreateSubAppOptions */
1145
-
1146
- /**
1147
- * 创建 vue 应用的描述对象
1148
- * @param {Record<string, any>} pkg
1149
- * @param {ICreateSubAppOptions} [options]
1150
- * @returns
1151
- */
1152
- function createVueSubApp(pkg, options) {
1153
- return createSubApp(pkg, { frameworkType: 'vue' }, options);
1154
- }
1155
-
1156
- var ExternalsDefault = {
1157
-
1158
- vue: 'Vue',
1159
- axios: 'axios',
1160
- // vuex: 'Vuex',
1161
- // 'vue-dim'
1162
- pinia: 'Pinia',
1163
- 'pinia-plugin-persistedstate': 'piniaPluginPersistedstate',
1164
- 'vue-i18n': 'VueI18n',
1165
- 'vue-router': 'VueRouter',
1166
- 'element-plus': 'ElementPlus',
1167
- '@arkxio/ark-micro-core': 'ArkMicroCore',
1168
- '@arkxio/ark-lib-proxy': 'ArkLibProxy',
1169
- '@arkxio/ark-micro': 'ArkMicro',
1170
- '@arkxio/ark-plugin': 'ArkPlugin'
1171
- // 'echarts': 'echarts',
1172
- // 'tinymce': 'tinymce'
1173
- // "loadjs": { //将vue依赖 "外部化",不打包进组件库
1174
- // root: 'loadjs',
1175
- // commonjs: 'loadjs',
1176
- // commonjs2: 'loadjs',
1177
- // amd: 'loadjs'
1178
- // }
1179
-
1180
- };
1181
-
1182
- class ArkWebpackPlugin {
1183
-
1184
- constructor(options) {
1185
- this.metaOptions = options;
1186
- }
1187
-
1188
- apply(compiler) {
1189
- // 获取项目根目录路径
1190
- compiler.context;
1191
- const useExtractOptions = this.metaOptions;
1192
-
1193
- // 在每次编译开始时重置脚本索引计数器,确保文件名一致性
1194
- compiler.hooks.thisCompilation.tap('ArkResetScriptIdx', () => {
1195
- resetScriptIdx();
1196
- });
1197
-
1198
- compiler.hooks.thisCompilation.tap('ArkDynamicCdnChunkLoaderPlugin', (compilation) => {
1199
- compilation.hooks.processAssets.tap(
1200
- {
1201
- name: 'ArkDynamicCdnChunkLoaderPlugin',
1202
- stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
1203
- },
1204
- (assets) => {
1205
- // 新增分块文件过滤逻辑
1206
- const chunkFiles = new Set();
1207
- compilation.chunks.forEach(chunk => {
1208
- chunk.files.forEach(file => {
1209
- if (file.endsWith('.js')) chunkFiles.add(file);
1210
- });
1211
- });
1212
- // 调试日志
1213
- verbose('[分块文件列表]', Array.from(chunkFiles));
1214
-
1215
- Object.entries(assets).forEach(([filename, source]) => {
1216
- // 仅处理分块JS文件
1217
- if (!chunkFiles.has(filename)) return;
1218
-
1219
- // 检查资源内容有效性
1220
- if (
1221
- !source ||
1222
- !source.source ||
1223
- typeof source.source() !== 'string' ||
1224
- source.source().trim() === ''
1225
- ) {
1226
- console.warn(`资源 ${filename} 内容为空或无效,跳过处理`);
1227
- return;
1228
- }
1229
-
1230
- let content = source.source().toString();
1231
-
1232
- // 替换逻辑
1233
- // /(__webpack_require__\.l)\(url,/g)
1234
- // const webpackRequireLPattern = /(__webpack_require__\.l)\(url,/g;
1235
- // if (webpackRequireLPattern.test(content)) {
1236
- // verbose('检测到 __webpack_require__.l 模式');
1237
- // content = content.replace(
1238
- // webpackRequireLPattern,
1239
- // `(function(url, done, key, chunkId) {
1240
- // if (typeof window.ArkConfig !== 'undefined' && window.ArkConfig.cdnHost) {
1241
- // if (!url.startsWith(window.ArkConfig.cdnHost) && url.startsWith('https://unpkg.com')) {
1242
- // url = window.ArkConfig.cdnHost + url.slice('https://unpkg.com'.length);
1243
- // }
1244
- // }
1245
- // return $1(url, done, key, chunkId);
1246
- // })(url,`
1247
- // );
1248
- const webpackRequireLPattern = /"(https:\/\/unpkg\.com)/g;
1249
- if (webpackRequireLPattern.test(content)) {
1250
- verbose('检测到 unpkg.com 模式');
1251
- content = content.replace(
1252
- webpackRequireLPattern,
1253
- `window.ArkConfig.cdnHost + "`
1254
- );
1255
-
1256
- // 更新资源
1257
- compilation.updateAsset(
1258
- filename,
1259
- new compiler.webpack.sources.RawSource(content)
1260
- );
1261
- }
1262
-
1263
- });
1264
- }
1265
- );
1266
- });
1267
-
1268
- compiler.hooks.emit.tapPromise('ArkMetaJsonGeneratorPlugin', async (compilation) => {
1269
- // 注入环境判断脚本
1270
- const isDev = process.env.NODE_ENV === 'development';
1271
-
1272
- // 收集所有生成的JS文件
1273
- const outputPath = compilation.options.output.publicPath || '';
1274
- const jsFiles = [];
1275
-
1276
- // Webpack 5 正确的API:使用 getAssets()
1277
- const assets = compilation.getAssets ? compilation.getAssets() : compilation.assets;
1278
-
1279
- // 遍历所有资产,收集JS文件
1280
- assets.forEach((asset) => {
1281
- const assetName = asset.name || asset;
1282
- if (assetName.endsWith('.js')) {
1283
- console.log('Found JS file:', assetName);
1284
- // 排除 ark-meta.json 和 ark_userChunk 文件
1285
- if (assetName.indexOf('js/app.') !== -1 ||
1286
- assetName.indexOf('js/runtime.') !== -1 ||
1287
- assetName.indexOf('js/vendors.') !== -1 ||
1288
- assetName.indexOf('js/main.') !== -1) {
1289
- jsFiles.push(outputPath + assetName);
1290
- }
1291
- }
1292
- });
1293
-
1294
- console.log('Final JS files for chunkJsSrcList:', jsFiles);
1295
-
1296
- // 创建 customAssets 数组用于收集自定义资产(如从 HTML 内联 script 提取的代码)
1297
- const customAssets = [];
1298
-
1299
- await extractArkMetaJson({
1300
- ...useExtractOptions,
1301
- chunkJsFiles: jsFiles,
1302
- isDev: isDev,
1303
- compilation: compilation,
1304
- customAssets: customAssets
1305
- });
1306
-
1307
- return Promise.resolve();
1308
- });
1309
- }
1310
1300
  }
1311
1301
 
1312
- var index = {
1313
- cst,
1314
- check,
1315
- baseUtils,
1316
- createReactSubApp,
1317
- createVueSubApp,
1318
- createLibSubApp,
1319
- extractArkMetaJson,
1320
- ExternalsDefault,
1321
- ArkWebpackPlugin
1302
+ var index = {
1303
+ cst,
1304
+ check,
1305
+ baseUtils,
1306
+ createReactSubApp,
1307
+ createVueSubApp,
1308
+ createLibSubApp,
1309
+ extractArkMetaJson,
1310
+ ExternalsDefault,
1311
+ ArkWebpackPlugin
1322
1312
  };
1323
1313
 
1324
1314
  exports.ArkWebpackPlugin = ArkWebpackPlugin;