@arkxio/ark-dev-utils 0.1.0

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * for getting pretty format multi line string when use \`...\`
3
+ * this function will remove indent of every line automatically
4
+ * @param {string} mayLineBreakStr
5
+ * @param {'MULTI' | 'ONE'} [mode='MULTI']
6
+ * @returns
7
+ * ```
8
+ * // usage 1, process multi lines with mode='MULTI' by default
9
+ * pfstr(`
10
+ * line1 line1 line1,
11
+ * line2 line2 line2.
12
+ * `);
13
+ * // pass to console.log will print:
14
+ * line1 line1 line1,
15
+ * line2 line2 line2.
16
+ * // attention: end <br/> will be removed automatically in MULTI mode
17
+ * pfstr(`
18
+ * line1 line1 line1,<br/>
19
+ * line2 line2 line2.
20
+ * `);
21
+ * // pass to console.log will print:
22
+ * line1 line1 line1,
23
+ * line2 line2 line2.
24
+ *
25
+ * // usage 2, set mode='ONE' to get no line break string
26
+ * pfstr(`
27
+ * line1 line1 line1,
28
+ * line2 line2 line2.
29
+ * `, 'ONE');
30
+ * // pass to console.log will print:
31
+ * line1 line1 line1, line2 line2 line2.
32
+ *
33
+ * // usage 3, add <br/> to control line break
34
+ * pfstr(`
35
+ * line1 line1 line1,
36
+ * line2 line2 line2,<br/>
37
+ * line3 line3 line3.
38
+ * `, 'ONE');
39
+ * // pass to console.log will print:
40
+ * line1 line1 line1, line2 line2 line2,
41
+ * line3 line3 line3.
42
+ * ```
43
+ */
44
+ export function pfstr(/** @type string */ mayLineBreakStr, mode = 'MULTI') {
45
+ // MULTI ONE
46
+ const lines = mayLineBreakStr.split('\n');
47
+ const lastIdx = lines.length - 1;
48
+
49
+ const formatLine = lines
50
+ .map((line, idx) => {
51
+ if (!line && (idx === 0 || idx === lastIdx)) {
52
+ return '';
53
+ }
54
+ const replaceBr = (/** @type string */ line, hasBrStr, noBrStr = '') => {
55
+ let result = line;
56
+ if (line.endsWith('<br/>')) {
57
+ result = line.substring(0, result.length - 5);
58
+ return `${result}${hasBrStr}`;
59
+ }
60
+ return `${result}${noBrStr}`;
61
+ };
62
+
63
+ // 此处暂时规避可选链写法,因 rollup 对此未处理,编译后上层使用会报错
64
+ // SyntaxError: Unexpected token '.'
65
+ const { trimStart } = line;
66
+ let result = trimStart ? line.trimStart() : line; // 去头部所有空格
67
+ if (mode === 'MULTI') {
68
+ return `${replaceBr(result, '')}\n`;
69
+ }
70
+ result = replaceBr(result, '\n', ' ');
71
+ return result;
72
+ })
73
+ .join('');
74
+ return formatLine;
75
+ }
@@ -0,0 +1,391 @@
1
+ /** @typedef {import('../../typings').SrcMap} SrcMap*/
2
+ /** @typedef {import('../../typings').IAssetOptions} IAssetOptions*/
3
+ /** @typedef {import('../../typings').IAssetInfo} IAssetInfo */
4
+ /** @typedef {import('../../typings').IInnerFillAssetListOptions} IInnerFillAssetListOptions */
5
+ import fs from 'fs';
6
+ import util from 'util';
7
+ import { slash } from '../base-utils/index';
8
+ import { noDupPush } from '../inner-utils/arr';
9
+ import { verbose } from '../inner-utils/index';
10
+ import { isNull } from '../inner-utils/obj';
11
+ import { pfstr } from '../inner-utils/str';
12
+ import { getAllFilePath } from './utils';
13
+
14
+ const writeFile = util.promisify(fs.writeFile);
15
+
16
+ /** jsdom 15 里去内联脚本 innerText 取不到,这里用此函数辅助 */
17
+ function getInnerText(/** @type {HTMLElement}} */ dom) {
18
+ return dom.innerText || dom.innerHTML || '';
19
+ }
20
+
21
+ function getDatasetVal(dataset, key, defaultValIfOnlyKey) {
22
+ const hasKey = Object.prototype.hasOwnProperty.call(dataset, key);
23
+ if (hasKey) {
24
+ return dataset[key] || defaultValIfOnlyKey;
25
+ }
26
+ return '';
27
+ }
28
+
29
+ function isRelativePath(path) {
30
+ if (path.startsWith('//')) return false;
31
+ return path.startsWith('/') || path.startsWith('./') || path.startsWith('../');
32
+ }
33
+
34
+ function buildAssetItem(tag, /** @type {IAssetInfo} */ assetInfo) {
35
+ const isLink = tag === 'link';
36
+ const { isBuildUrl, isNonBuildAndRelative, canAppend, el, url, innerText } = assetInfo;
37
+ let attrs = {};
38
+ if (url) {
39
+ attrs = isLink ? { href: url } : { src: url };
40
+ }
41
+
42
+ let tagVar = '';
43
+ if (isBuildUrl) {
44
+ tagVar = tag;
45
+ } else if (isNonBuildAndRelative) {
46
+ tagVar = isLink ? 'relativeLink' : 'relativeScript';
47
+ } else {
48
+ tagVar = isLink ? 'staticLink' : 'staticScript';
49
+ }
50
+
51
+ const assetItem = { tag: tagVar, append: canAppend, attrs };
52
+ if (innerText) {
53
+ assetItem.innerText = innerText;
54
+ }
55
+
56
+ const attrNames = el.getAttributeNames();
57
+ attrNames.forEach((name) => {
58
+ // src href 上面已记录真正的目标值,故移除
59
+ // data-helappend 只在提取元数据辅助计算 append 值时用到,故此处移除
60
+ if (['src', 'href', 'data-helappend'].includes(name)) return;
61
+ attrs[name] = el.getAttribute(name);
62
+ });
63
+
64
+ return assetItem;
65
+ }
66
+
67
+ let custScriptIdx = 0;
68
+
69
+ /**
70
+ * @param {HTMLScriptElement | HTMLStyleElement} childDom
71
+ * @param {string} fileType
72
+ * @param {IInnerFillAssetListOptions} options
73
+ */
74
+ async function writeInnerText(childDom, fileType, options) {
75
+ const { homePage, buildDirFullPath } = options;
76
+ const innerText = getInnerText(childDom);
77
+ if (!innerText) return '';
78
+
79
+ verbose(`found a user customized ${fileType} tag node in html, try extract its content and write them to local fs`);
80
+ custScriptIdx += 1;
81
+ const scriptName = `ark_userChunk_${custScriptIdx}.${fileType}`;
82
+ const fileAbsolutePath = `${buildDirFullPath}/${scriptName}`;
83
+ const fileWebPath = `${slash.noEnd(homePage)}/${scriptName}`;
84
+
85
+ await writeFile(fileAbsolutePath, innerText);
86
+ verbose(`write done, the web file will be ${fileWebPath} later`);
87
+ return fileWebPath;
88
+ }
89
+
90
+ /**
91
+ * @param {string} url
92
+ * @param {IAssetOptions} options
93
+ */
94
+ function getAssetInfo(url, options) {
95
+ const { homePage, extractMode, enableRelativePath, enableAssetInnerText, el, innerText } = options;
96
+ const { dataset = {} } = el;
97
+ // 是构建生成的产物路径,无 url 的当做是内联的处理,isBuildUrl 强制为 true
98
+ const isBuildUrl = url ? url.startsWith(homePage) : true;
99
+ const isRelative = isRelativePath(url);
100
+ // 是 homePage 之外相对路径导入的产物路径
101
+ const isNonBuildAndRelative = !isBuildUrl && isRelative;
102
+ const isStatic = !isBuildUrl && !isRelative;
103
+ const isIcoAsset = url.endsWith('.ico');
104
+ // 设置了 extractMode 为 build 和 build_no_html 时,当前产物路径是非构建生成的,则直接忽略,不记录到 assetList 数据里
105
+ const ignoreAddToAssetList = !isBuildUrl && (extractMode === 'build' || extractMode === 'build_no_html');
106
+ let allowAddToAssetList = !ignoreAddToAssetList;
107
+ if (!allowAddToAssetList) {
108
+ verbose(` >>> ignore add asset [${url}] to assetList by extractMode=${extractMode}`);
109
+ }
110
+
111
+ // 未显式设置 data-helappend 时,helappend 默认值会走内部逻辑来决定如何赋值
112
+ let helAppendValOfDataset = getDatasetVal(dataset, 'helappend', '1');
113
+ let helAppendValOfInnerLogic = '';
114
+ if (helAppendValOfDataset && !['1', '0'].includes(helAppendValOfDataset)) {
115
+ throw new Error(`found invalid helappend value [${helAppendValOfDataset}], only accpet 1 or 0 currently`);
116
+ }
117
+
118
+ const ex = dataset.helex || '';
119
+ if (isNonBuildAndRelative) {
120
+ if (isIcoAsset && !helAppendValOfDataset) {
121
+ helAppendValOfDataset = '0'; // ico 文件特殊处理,默认是不加载的
122
+ } else if (ex && !helAppendValOfDataset) {
123
+ helAppendValOfDataset = '1'; // 标记了 ex 的文件特殊处理,默认需要加载,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
124
+ }
125
+
126
+ // 如下面错误描述所示,在既没有设置 enableRelativePath=true,又没有显式的标记 data-helappend 的情况下
127
+ // 不允许 homePage 之外的相对路径导入的资源存在
128
+ // 所以对于此类 homePage 之外的相对路径导入的资源,要么用户设置 enableRelativePath=true,要么显式的标记 data-helappend
129
+ // 设置 enableRelativePath=true 后,优先读可能已存在的 data-helappend 值,没有则默认为 0,表示不加载
130
+ // 不设置 enableRelativePath=true 的话,则需要用户一定标记 data-helappend 值
131
+ if (!enableRelativePath && !helAppendValOfDataset) {
132
+ throw new Error(
133
+ pfstr(`
134
+ found asset url [${url}] is a relative path, it is obviously not a valid url for cdn architecture deploy!
135
+ 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:
136
+ 1. pass enableRelativePath true to ark-dev-utils.extractArkMetaJson method options.
137
+ 2. add data-helappend="0" on the asset dom attribute to tell ark-dev-utils ignore this asset.
138
+
139
+ ark-dev-utils will mark this url as relativeLink or relativeScript, and set append as false,
140
+ if you want sdk append this asset, you can explicitly add data-helappend="1" on the asset dom attribute.
141
+ a demo will be like:<script src="./a/b.js" data-helappend="1"></script>,<br/>
142
+ note that the asset will depend on your host site seriously under this situation.
143
+ `),
144
+ );
145
+ }
146
+
147
+ // 构建时设置 enableRelativePath=true,则允许此类【homePage之外相对路径导入的产物】加入到资源清单列表里
148
+ allowAddToAssetList = true;
149
+ helAppendValOfInnerLogic = helAppendValOfDataset || '0'; // 没有显示设定 data-helappend 时默认标记为不加载
150
+ } else if (isIcoAsset) {
151
+ helAppendValOfInnerLogic = '0'; // ico 文件特殊处理,默认是不加载的
152
+ } else if (isStatic) {
153
+ if (ex) {
154
+ // 对于标记了 helex 的元素,默认是 append 的,能不能真的追加到文档上,取决于 @arkxio/ark-micro 的重复检测结果
155
+ // 重复则不追加,不重复则追加
156
+ helAppendValOfInnerLogic = helAppendValOfDataset || '1';
157
+ } else {
158
+ helAppendValOfInnerLogic = '0';
159
+ }
160
+ } else {
161
+ // isBuild
162
+ helAppendValOfInnerLogic = '1'; // 标记为可加载
163
+ }
164
+
165
+ const helAppendVal = helAppendValOfDataset || helAppendValOfInnerLogic;
166
+ const helAppend = helAppendVal === '1';
167
+ if (ex && !helAppend) {
168
+ // 设置了 helex 但同时设置了不加载,会造成歧义,当前版本是不允许的
169
+ throw new Error(
170
+ pfstr(`
171
+ found conflict setting for helex: [data-helex="${ex}"]、[data-helappend="${helAppendVal}"],
172
+ remove data-helappend( append is true for helex by default ) or set data-helappend="1"!
173
+ `),
174
+ );
175
+ }
176
+
177
+ return {
178
+ url,
179
+ el,
180
+ isBuildUrl,
181
+ isNonBuildAndRelative,
182
+ canAppend: helAppend,
183
+ allowAddToAssetList,
184
+ innerText: enableAssetInnerText ? innerText : '',
185
+ };
186
+ }
187
+
188
+ /**
189
+ * 提取link、script标签数据并填充到目标assetList
190
+ * @param {HTMLCollectionOf<HTMLScriptElement>} doms
191
+ * @param {IInnerFillAssetListOptions} options
192
+ */
193
+ export async function fillAssetList(doms, options) {
194
+ const {
195
+ homePage,
196
+ enableReplaceDevJs = true,
197
+ enableRelativePath = false,
198
+ enableAssetInnerText = false,
199
+ enablePrefixHomePage = false,
200
+ srcMap,
201
+ isHead,
202
+ } = options;
203
+ const {
204
+ headAssetList,
205
+ bodyAssetList,
206
+ extractMode,
207
+ chunkCssSrcList,
208
+ staticCssSrcList,
209
+ relativeCssSrcList,
210
+ chunkJsSrcList,
211
+ staticJsSrcList,
212
+ relativeJsSrcList,
213
+ } = srcMap;
214
+ const assetList = isHead ? headAssetList : bodyAssetList;
215
+
216
+ const len = doms.length;
217
+ const replaceContentList = [];
218
+ verbose(`extractMode is ${extractMode}`);
219
+ verbose(`enableAssetInnerText is ${enableAssetInnerText}`);
220
+
221
+ const pushToSrcList = (assetType, /** @type {IAssetInfo} */ assetInfo) => {
222
+ const { isBuildUrl, isNonBuildAndRelative, url } = assetInfo;
223
+ const isAllExtractMode = extractMode === 'all' || extractMode === 'all_no_html';
224
+
225
+ if (assetType === 'css') {
226
+ if (isBuildUrl) {
227
+ return noDupPush(chunkCssSrcList, url);
228
+ }
229
+ if (isAllExtractMode) {
230
+ const list = isNonBuildAndRelative ? relativeCssSrcList : staticCssSrcList;
231
+ return noDupPush(list, url);
232
+ }
233
+ }
234
+
235
+ if (isBuildUrl) {
236
+ return noDupPush(chunkJsSrcList, url);
237
+ }
238
+ if (isAllExtractMode) {
239
+ const list = isNonBuildAndRelative ? relativeJsSrcList : staticJsSrcList;
240
+ return noDupPush(list, url);
241
+ }
242
+ };
243
+
244
+ const mayPrefixHomePage = (url) => {
245
+ if (enablePrefixHomePage) {
246
+ if (url.startsWith('/') && !url.startsWith('//')) {
247
+ verbose(` >>> prefix homePage [${homePage}] for url [${url}]`);
248
+ const finalUrl = `${slash.noEnd(homePage)}${url}`;
249
+ replaceContentList.push({ toMatch: `="${url}"`, toReplace: `="${finalUrl}"` });
250
+ return finalUrl;
251
+ }
252
+ }
253
+ return url;
254
+ };
255
+
256
+ for (let i = 0; i < len; i++) {
257
+ const childDom = doms[i];
258
+ const { tagName, dataset = {} } = childDom;
259
+ const innerText = getInnerText(childDom);
260
+ let toPushAsset = null;
261
+ let allowAddToAssetList = false;
262
+ const assetOptions = { el: childDom, innerText, homePage, extractMode, enableRelativePath, enableAssetInnerText };
263
+
264
+ if (!['LINK', 'SCRIPT', 'STYLE'].includes(tagName)) {
265
+ continue;
266
+ }
267
+ if (!isNull(dataset)) {
268
+ verbose(`found ${tagName} dataset`, dataset);
269
+ }
270
+
271
+ if (tagName === 'LINK') {
272
+ const { hreflang = '', href: oriHref } = childDom;
273
+ verbose(`analyze link href:[${oriHref}]`);
274
+ if (!oriHref) continue;
275
+
276
+ let href = mayPrefixHomePage(oriHref);
277
+ verbose(`analyze link [${href}]`);
278
+ // 一些使用了老版本cra的项目,这两个href 在修改了 publicPath 后也不被添加前缀,这里做一下修正
279
+ const legacyHrefs = ['/manifest.json', '/favicon.ico'];
280
+ if (legacyHrefs.includes(href)) {
281
+ const oldHref = href;
282
+ href = `${homePage}${href}`;
283
+ replaceContentList.push({ toMatch: href, toReplace: href });
284
+ verbose(`replace link [${oldHref}] to [href]`);
285
+ }
286
+
287
+ const assetInfo = getAssetInfo(href, assetOptions);
288
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
289
+ // 供 shadow-dom 或其他需要知道当前应用所有样式列表的场景用
290
+ if (href.endsWith('.css')) {
291
+ pushToSrcList('css', assetInfo);
292
+ }
293
+ toPushAsset = buildAssetItem('link', assetInfo);
294
+ } else if (tagName === 'SCRIPT') {
295
+ const { src } = childDom;
296
+ verbose(`analyze script src:[${src}], innerText:[${innerText}]`);
297
+
298
+ if (!src && !innerText) {
299
+ continue;
300
+ }
301
+
302
+ if (!src && innerText && enableAssetInnerText) {
303
+ verbose('user set enableAssetInnerText = true and found SCRIPT node innerText');
304
+ const assetInfo = getAssetInfo('', assetOptions);
305
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
306
+ toPushAsset = buildAssetItem('script', assetInfo);
307
+ } else {
308
+ let targetSrc = src;
309
+ if (!targetSrc) {
310
+ targetSrc = await writeInnerText(childDom, 'js', options);
311
+ }
312
+ if (!targetSrc) continue;
313
+
314
+ targetSrc = mayPrefixHomePage(targetSrc);
315
+ if (enableReplaceDevJs) {
316
+ // 替换用户的 development 模式的 react 相关包体
317
+ if (targetSrc.endsWith('react.dev.js')) {
318
+ const toReplace = targetSrc.replace('react.dev.js', 'react.js');
319
+ replaceContentList.push({ toMatch: targetSrc, toReplace });
320
+ targetSrc = toReplace;
321
+ }
322
+ // 替换用户的 development 模式的 vue 相关包体
323
+ if (targetSrc.endsWith('vue.dev.js')) {
324
+ const toReplace = targetSrc.replace('vue.dev.js', 'vue.js');
325
+ replaceContentList.push({ toMatch: targetSrc, toReplace });
326
+ targetSrc = toReplace;
327
+ }
328
+ }
329
+
330
+ const assetInfo = getAssetInfo(targetSrc, assetOptions);
331
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
332
+
333
+ pushToSrcList('js', assetInfo);
334
+ toPushAsset = buildAssetItem('script', assetInfo);
335
+ }
336
+ } else if (tagName === 'STYLE') {
337
+ verbose(`analyze style innerText:[${innerText}]`);
338
+
339
+ if (innerText && enableAssetInnerText) {
340
+ verbose('user set enableAssetInnerText = true and found STYLE node innerText');
341
+ const assetInfo = getAssetInfo('', assetOptions);
342
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
343
+ toPushAsset = buildAssetItem('style', assetInfo);
344
+ } else {
345
+ // style 标签转换为 css 文件存起来,以便让 @arkxio/ark-micro 用 link 标签加载
346
+ let href = await writeInnerText(childDom, 'css', options);
347
+ if (!href) continue;
348
+
349
+ href = mayPrefixHomePage(href);
350
+ const assetInfo = getAssetInfo(href, assetOptions);
351
+ allowAddToAssetList = assetInfo.allowAddToAssetList;
352
+
353
+ pushToSrcList('css', assetInfo);
354
+ toPushAsset = buildAssetItem('link', assetInfo);
355
+ }
356
+ }
357
+
358
+ if (toPushAsset && allowAddToAssetList) {
359
+ assetList.push(toPushAsset);
360
+ }
361
+ }
362
+
363
+ return replaceContentList;
364
+ }
365
+
366
+ /**
367
+ * @param {IInnerFillAssetListOptions} options
368
+ */
369
+ export async function fillAssetListByDist(options) {
370
+ const { homePage, srcMap, buildDirFullPath } = options;
371
+ const fileFullPathList = getAllFilePath(buildDirFullPath);
372
+ verbose('filePathList', fileFullPathList);
373
+
374
+ fileFullPathList.forEach((fileAbsolutePath) => {
375
+ // 获取文件处于build目录下的相对路径,形如:
376
+ // /static/js/runtime-main.66c45929.js
377
+ // /asset-manifest.json
378
+ const filePathUnderBuild = fileAbsolutePath.split(buildDirFullPath)[1];
379
+ // 拼出 web 路径
380
+ const fileWebPath = `${slash.noEnd(homePage)}${slash.start(filePathUnderBuild)}`;
381
+
382
+ // 补上剩余的 css 文件路径
383
+ if (fileWebPath.endsWith('.css')) {
384
+ noDupPush(srcMap.chunkCssSrcList, fileWebPath);
385
+ } else if (fileWebPath.endsWith('.js')) {
386
+ noDupPush(srcMap.chunkJsSrcList, fileWebPath);
387
+ } else {
388
+ noDupPush(srcMap.otherSrcList, fileWebPath);
389
+ }
390
+ });
391
+ }
@@ -0,0 +1,73 @@
1
+ import fs from 'fs';
2
+ import { verbose } from '../inner-utils/index';
3
+ import { fillAssetListByDist } from './fillAssetList';
4
+ import { parseIndexHtml } from './parse';
5
+ import { getIndexHtmlFileName, makeArkMetaJson } from './utils';
6
+ import path from "path";
7
+ import {noDupPush} from "../inner-utils/arr";
8
+
9
+ /**
10
+ * 从 index.html 提取资源的描述数据,包含 htmlContent、srcMap
11
+ * @param {import('../../typings').IUserExtractOptions} userExtractOptions
12
+ */
13
+ export default async function extractArkMetaJson(userExtractOptions) {
14
+ const { buildDirFullPath, writeMetaJsonToDist = true } = userExtractOptions;
15
+ const appInfo = userExtractOptions.appInfo || userExtractOptions.subApp;
16
+ const indexHtmlName = userExtractOptions.indexHtmlName || 'index.html';// getIndexHtmlFileName(buildDirFullPath);
17
+
18
+ if (!appInfo) {
19
+ throw new Error('appInfo should be supplied in ver 3.0+ ark-dev-utils: extractArkMetaJson({appInfo, ...})');
20
+ }
21
+
22
+ const { homePage } = appInfo;
23
+ const options = { ...userExtractOptions, appInfo, subApp: appInfo, indexHtmlName };
24
+
25
+ verbose(`start extractArkMetaJson, appHomePage is [${homePage}]`);
26
+ // 分析 html 入口,提取 sdk 加载需要的资源清单
27
+ const parsedRet = await parseIndexHtml(options);
28
+ // 分析构建产物目录,提取剩余的资源清单补充到 chunkJsSrcList chunkCssSrcList 下,以便描述出应用的所有构建产物的资源路径
29
+ // fillAssetListByDist({ ...userExtractOptions, srcMap: parsedRet.srcMap, homePage, buildDirFullPath });
30
+ if (userExtractOptions.chunkJsFiles) {
31
+ for (const chunkJsFile of userExtractOptions.chunkJsFiles) {
32
+ noDupPush(parsedRet.srcMap.chunkJsSrcList, chunkJsFile);
33
+
34
+ noDupPush(parsedRet.srcMap.bodyAssetList, {
35
+ tag: 'script',
36
+ append: true,
37
+ attrs: {
38
+ src: chunkJsFile,
39
+ type: 'text/javascript',
40
+ },
41
+ });
42
+ }
43
+ }
44
+
45
+ // 有替换内容生成,则将 index.html 内容重写,让后续上传 cdn 步骤上传的是替换后的文件内容
46
+ if (parsedRet.hasReplacedContent) {
47
+ const htmlFilePath = `${buildDirFullPath}/${indexHtmlName}`;
48
+ fs.writeFileSync(htmlFilePath, parsedRet.htmlContent, { encoding: 'utf-8' });
49
+ }
50
+
51
+ const arkMeta = makeArkMetaJson(options, parsedRet);
52
+ verbose('userExtractOptions.isDev', userExtractOptions.isDev)
53
+ if(userExtractOptions.isDev) {
54
+ // 开发模式:写入内存文件系统
55
+ const metaFilename = 'ark-meta.json';
56
+ verbose('[begin] write memory ark meta ' + metaFilename)
57
+
58
+ const metaContent = JSON.stringify(arkMeta, null, 2);
59
+ // 写入 webpack 内存文件系统
60
+ userExtractOptions.compilation.assets[metaFilename] = {
61
+ source: () => metaContent,
62
+ size: () => metaContent.length
63
+ };
64
+ verbose('[finish] write memory ark meta ' + metaFilename)
65
+ }else if (writeMetaJsonToDist) {
66
+ // 生产模式:写入物理文件
67
+ const arkMetaJsonFile = `${buildDirFullPath}/ark-meta.json`;
68
+ verbose('finish write ark meta')
69
+ fs.writeFileSync(arkMetaJsonFile, JSON.stringify(arkMeta, null, 2));
70
+ }
71
+
72
+ return arkMeta;
73
+ }
@@ -0,0 +1,56 @@
1
+ /** @typedef {import('../../typings').SrcMap} SrcMap */
2
+ /** @typedef {import('../../typings').IUserExtractOptions} IUserExtractOptions */
3
+ /** @typedef {import('../../typings').IInnerFillAssetListOptions} IInnerFillAssetListOptions */
4
+ import * as fs from 'fs';
5
+ import jsdom from 'jsdom';
6
+ import * as util from 'util';
7
+ import cst from '../configs/consts';
8
+ import { verbose } from '../inner-utils/index';
9
+ import { fillAssetList } from './fillAssetList';
10
+ import { makeAppVersionSrcMap } from './utils';
11
+
12
+ const readFile = util.promisify(fs.readFile);
13
+ const { JSDOM } = jsdom;
14
+
15
+ /**
16
+ * @param {IUserExtractOptions} extractOptions
17
+ */
18
+ export async function parseIndexHtml(extractOptions) {
19
+ const { appInfo, indexHtmlPath, extractMode = 'all', indexHtmlName = cst.DEFAULT_HTML_INDEX_NAME } = extractOptions;
20
+ const { name, homePage } = appInfo;
21
+ const htmlFilePath = `${indexHtmlPath}`;
22
+ verbose(`start to parse ${name} index.html file [${indexHtmlPath}]`);
23
+
24
+ let htmlContent = await readFile(htmlFilePath, { encoding: 'UTF-8' });
25
+ const srcMap = makeAppVersionSrcMap(extractOptions);
26
+
27
+ const dom = new JSDOM(htmlContent);
28
+ const { head, body } = dom.window.document;
29
+ /** @type {IInnerFillAssetListOptions} */
30
+ const fillAssetListOptions = { srcMap, homePage, ...extractOptions };
31
+ const [replaceContentListOfHead, replaceContentLisOfBody] = await Promise.all([
32
+ fillAssetList(head.children, { ...fillAssetListOptions, isHead: true }),
33
+ fillAssetList(body.children, fillAssetListOptions),
34
+ ]);
35
+
36
+ replaceContentListOfHead.forEach((item) => {
37
+ htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
38
+ });
39
+ replaceContentLisOfBody.forEach((item) => {
40
+ htmlContent = htmlContent.replace(item.toMatch, item.toReplace);
41
+ });
42
+
43
+ const shouldRecordHtmlContent = extractMode === 'all' || extractMode === 'build';
44
+ const htmlContentVar = shouldRecordHtmlContent ? htmlContent : '';
45
+ if (!shouldRecordHtmlContent) {
46
+ verbose(`user set extractMode='${extractMode}', dev-utils will ignore write version.html_content`);
47
+ }
48
+
49
+ const hasReplacedContent = replaceContentListOfHead.length || replaceContentLisOfBody.length;
50
+ const parsedRet = { srcMap, htmlContent: htmlContentVar, hasReplacedContent };
51
+ verbose(`parse app [${name}] index.html file done!`);
52
+ verbose('replaceContentListOfHead: ', replaceContentListOfHead);
53
+ verbose('replaceContentLisOfBody: ', replaceContentLisOfBody);
54
+ verbose('parsedRet: ', parsedRet);
55
+ return parsedRet;
56
+ }