hexo-swpp 2.3.0-beta.1 → 2.3.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,500 @@
1
+ module.exports = (hexo, config, pluginConfig, swRules) => {
2
+ const logger = require('hexo-log')()
3
+ const fs = require('fs')
4
+ const fetch = require('node-fetch')
5
+ const nodePath = require('path')
6
+ const crypto = require("crypto")
7
+ const cheerio = require('cheerio')
8
+ const postcss = require('postcss')
9
+ const {
10
+ cacheList,
11
+ modifyRequest
12
+ } = swRules
13
+ const root = config.url + (config.root ?? '/')
14
+ const domain = new URL(root).hostname
15
+
16
+ // noinspection JSUnresolvedVariable
17
+ hexo.extend.console.register('swpp', '生成前端更新需要的 json 文件以及相关缓存', {}, async () => {
18
+ // noinspection JSUnresolvedVariable
19
+ if (!fs.existsSync(config.public_dir))
20
+ return logger.info('未检测到发布目录,跳过 swpp 执行')
21
+ const cachePath = 'cacheList.json'
22
+ const updatePath = 'update.json'
23
+ const oldCache = await getJsonFromNetwork(cachePath)
24
+ const oldUpdate = await getJsonFromNetwork(updatePath)
25
+ const newCache = await buildNewJson(cachePath)
26
+ const dif = compare(oldCache, newCache)
27
+ buildUpdateJson(updatePath, dif, oldUpdate)
28
+ })
29
+
30
+
31
+ /** 遍历指定目录下的所有文件 */
32
+ const eachAllFile = (root, cb) => {
33
+ const stats = fs.statSync(root)
34
+ if (stats.isFile()) cb(root)
35
+ else {
36
+ const files = fs.readdirSync(root)
37
+ files.forEach(it => eachAllFile(nodePath.join(root, it), cb))
38
+ }
39
+ }
40
+
41
+ /** 判断指定文件是否需要排除 */
42
+ const isExclude = pathname => {
43
+ // noinspection JSUnresolvedVariable
44
+ for (let reg of pluginConfig.json.exclude) {
45
+ if (pathname.match(new RegExp(reg, 'i'))) return true
46
+ }
47
+ return false
48
+ }
49
+
50
+ const isSkipFetch = url => {
51
+ const skipList = pluginConfig.external?.skip
52
+ if (!skipList) return false
53
+ for (let reg of skipList) {
54
+ if (url.match(new RegExp(reg))) return true
55
+ }
56
+ return false
57
+ }
58
+
59
+ /**
60
+ * 构建 md5 缓存表并写入到发布目录中
61
+ *
62
+ * 格式为 `{"[path]": "[md5Value]"}`
63
+ *
64
+ * @param path 相对于根目录的路径
65
+ * @return {Promise<Object>} 生成的 json 对象
66
+ */
67
+ const buildNewJson = path => new Promise(resolve => {
68
+ const result = {} // 存储新的 MD5 表
69
+ // noinspection JSUnresolvedVariable
70
+ const removeIndex = config.pretty_urls?.trailing_index
71
+ // noinspection JSUnresolvedVariable
72
+ const removeHtml = config.pretty_urls?.trailing_html
73
+ const taskList = [] // 拉取任务列表
74
+ const cache = new Set() // 已经计算过的文件
75
+ // noinspection JSUnresolvedVariable
76
+ eachAllFile(config.public_dir, path => {
77
+ if (!fs.existsSync(path)) return logger.error(`${path} 不存在!`)
78
+ let endIndex
79
+ if (removeIndex && path.endsWith('/index.html')) endIndex = path.length - 10
80
+ else if (removeHtml && path.endsWith('.html')) endIndex = path.length - 5
81
+ else endIndex = path.length
82
+ const url = new URL(nodePath.join(root, path.substring(7, endIndex)))
83
+ if (isExclude(url.href)) return
84
+ let content = null
85
+ if (findCache(url)) {
86
+ content = fs.readFileSync(path, 'utf-8')
87
+ const key = decodeURIComponent(url.pathname)
88
+ result[key] = crypto.createHash('md5').update(content).digest('hex')
89
+ }
90
+ // 外链监控
91
+ const external = pluginConfig.external
92
+ if (!pluginConfig.external?.enable) return
93
+ const indexOf = (str, ...chars) => {
94
+ let result = str.length
95
+ chars.forEach(it => {
96
+ const index = str.indexOf(it)
97
+ result = Math.min(result, index < 0 ? result : index)
98
+ })
99
+ return result
100
+ }
101
+ const lastIndexOf = (str, ...chars) => {
102
+ let result = -1
103
+ chars.forEach(it => result = Math.max(result, str.lastIndexOf(it)))
104
+ return result
105
+ }
106
+ // 处理指定链接
107
+ const handleLink = link => {
108
+ // 跳过本地文件的计算
109
+ if (!link.match(/^(http|\/\/)/) || cache.has(link)) return
110
+ cache.add(link)
111
+ const url = new URL(link.startsWith('/') ? `http:${link}` : link)
112
+ if (url.hostname === domain || !findCache(url) || isExclude(url.href)) return
113
+ if (isSkipFetch(url.href)) result[decodeURIComponent(link)] = '0'
114
+ else taskList.push(
115
+ fetchFile(link)
116
+ .then(response => response.text())
117
+ .then(text => {
118
+ const key = decodeURIComponent(link)
119
+ result[key] = crypto.createHash('md5').update(text).digest('hex')
120
+ if (key.endsWith('.js')) handleJsContent(text)
121
+ else if (key.endsWith('.css')) handleCssContent(text)
122
+ }).catch(err => logger.error(`拉取 ${err.url} 时出现 ${err.status ?? '未知'} 异常:${err}`))
123
+ )
124
+ }
125
+ // 处理指定 JS
126
+ const handleJsContent = text => {
127
+ // noinspection JSUnresolvedVariable
128
+ if (!external.js) return
129
+ if (cache.has(text)) return
130
+ cache.add(text)
131
+ external.js.forEach(it => {
132
+ // noinspection JSUnresolvedVariable
133
+ const reg = new RegExp(`${it.head}(['"\`])(.*?)\\1${it.tail}`, 'mg')
134
+ text.match(reg)?.forEach(content => {
135
+ try {
136
+ const start = indexOf(content, "'", '"', '`') + 1
137
+ const end = lastIndexOf(content, "'", '"', '`')
138
+ const link = content.substring(start, end)
139
+ if (!link.match(/['"$`]/)) handleLink(link)
140
+ } catch (e) {
141
+ logger.error(`SwppJsHandler: 处理 ${content} 时出现异常`)
142
+ logger.error(e)
143
+ }
144
+ })
145
+ })
146
+ }
147
+ // 处理 CSS 内容
148
+ const handleCssContent = text => {
149
+ if (cache.has(text)) return
150
+ cache.add(text)
151
+ postcss.parse(text).walkDecls(decl => {
152
+ if (decl.value.includes('url')) {
153
+ decl.value.match(/url\(([^)]+)\)/g)
154
+ .map(it => it.match(/^(url\(['"])/) ? it.substring(5, it.length - 2) : it.substring(4, it.length - 1))
155
+ .forEach(link => handleLink(link))
156
+ }
157
+ })
158
+ }
159
+ // 如果是 html 则获取所有 script 和 link 标签拉取的文件
160
+ if (path.endsWith('/') || path.endsWith('.html')) {
161
+ if (!content) content = fs.readFileSync(path, 'utf-8')
162
+ const html = cheerio.load(content)
163
+ html('script[src]')
164
+ .map((i, ele) => html(ele).attr('src'))
165
+ .each((i, it) => handleLink(it))
166
+ html('link[href]')
167
+ .map((i, ele) => html(ele).attr('href'))
168
+ .each((i, it) => handleLink(it))
169
+ html('script:not([src])')
170
+ .map((i, ele) => html(ele).text())
171
+ .each((i, text) => handleJsContent(text))
172
+ html('style')
173
+ .map((i, ele) => html(ele).text())
174
+ .each((i, text) => handleCssContent(text))
175
+ } else if (path.endsWith('.js')) {
176
+ if (!content) content = fs.readFileSync(path, 'utf-8')
177
+ handleJsContent(content)
178
+ } else if (path.endsWith('.css')) {
179
+ if (!content) content = fs.readFileSync(path, 'utf-8')
180
+ handleCssContent(content)
181
+ }
182
+ })
183
+ Promise.all(taskList).then(() => {
184
+ // noinspection JSUnresolvedVariable
185
+ const publicRoot = config.public_dir
186
+ fs.writeFileSync(nodePath.join(publicRoot, path), JSON.stringify(result), 'utf-8')
187
+ logger.info(`Generated: ${path}`)
188
+ resolve(result)
189
+ })
190
+ })
191
+
192
+ /**
193
+ * 从网络拉取一个文件
194
+ * @param link 文件链接
195
+ * @returns {Promise<*>} response
196
+ */
197
+ const fetchFile = link => new Promise((resolve, reject) => {
198
+ link = replaceDevRequest(link)
199
+ // noinspection SpellCheckingInspection
200
+ fetch(link, {
201
+ headers: {
202
+ referer: new URL(link).hostname,
203
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.62'
204
+ },
205
+ timeout: pluginConfig.external.timeout ?? 1500
206
+ }).then(response => {
207
+ switch (response.status) {
208
+ case 200: case 301: case 302:
209
+ resolve(response)
210
+ break
211
+ default:
212
+ reject(response)
213
+ break
214
+ }
215
+ }).catch(err => {
216
+ err.url = link
217
+ reject(err)
218
+ })
219
+ })
220
+
221
+ /**
222
+ * 从网络拉取 json 文件
223
+ * @param path 文件路径(相对于根目录)
224
+ */
225
+ const getJsonFromNetwork = path => new Promise(resolve => {
226
+ const url = nodePath.join(root, path)
227
+ fetchFile(url)
228
+ .then(response => resolve(response.json()))
229
+ .catch(err => {
230
+ if (err.status === 404) {
231
+ logger.error(`拉取 ${err.url} 时出现 404,如果您是第一次构建请忽略这个错误`)
232
+ resolve()
233
+ } else throw err
234
+ })
235
+ })
236
+
237
+ /**
238
+ * 对比两个 md5 缓存表的区别
239
+ * @return [string] 需要更新的文件路径
240
+ */
241
+ const compare = (oldCache, newCache) => {
242
+ const result = []
243
+ if (!oldCache) return result
244
+ for (let path in oldCache) {
245
+ if (newCache[path] !== oldCache[path]) result.push(path)
246
+ }
247
+ return result
248
+ }
249
+
250
+ /** 判断指定资源是否需要合并 */
251
+ const isMerge = (pathname, tidied) => {
252
+ // noinspection JSUnresolvedVariable
253
+ const optional = pluginConfig.json.merge
254
+ const {tag_dir, archive_dir, category_dir} = config
255
+ if (!optional) return false
256
+ if (pathname.includes(`/${tag_dir}/`)) {
257
+ if (optional.tags ?? true)
258
+ return tidied.tags = true
259
+ } else if (pathname.includes(`/${archive_dir}/`)) {
260
+ if (optional.archives ?? true)
261
+ return tidied.archives = true
262
+ } else if (pathname.includes(`/${category_dir}/`)) {
263
+ if (optional.categories ?? true)
264
+ return tidied.categories = true
265
+ } else if (pathname.startsWith('/page/') || pathname.length <= 1) {
266
+ if (optional.index ?? true)
267
+ return tidied.index = true
268
+ } else {
269
+ const list = optional.custom
270
+ if (!list) return false
271
+ for (let reg of list) {
272
+ if (pathname.startsWith(`/${reg}/`))
273
+ return tidied.custom[reg] = true
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * 从一个字符串中提取最后两个 / 之间的内容
280
+ * @param it {string} 要操作的字符串
281
+ * @param keep {boolean} 是否保留最后一个 / 及其后面的内容
282
+ */
283
+ const clipPageName = (it, keep) => {
284
+ const end = it.lastIndexOf('/')
285
+ let index = end - 1
286
+ for (; index > 0; --index) {
287
+ if (it[index] === '/') break
288
+ }
289
+ return it.substring(index + 1, keep ? it.length : end)
290
+ }
291
+
292
+ /** 构建新的 update.json */
293
+ const buildUpdateJson = (name, dif, oldUpdate) => {
294
+ /** 将对象写入文件,如果对象为 null 或 undefined 则跳过写入 */
295
+ const writeJson = json => {
296
+ if (json) {
297
+ logger.info(`Generated: ${name}`)
298
+ fs.writeFileSync(`public/${name}`, JSON.stringify(json), 'utf-8')
299
+ }
300
+ }
301
+ // 读取拓展 json
302
+ const expand = fs.existsSync(name) ? JSON.parse(fs.readFileSync(name, 'utf-8')) : undefined
303
+ // 获取上次最新的版本
304
+ let oldVersion = oldUpdate?.info?.at(0)?.version ?? 0
305
+ if (typeof oldVersion !== 'number') {
306
+ // 当上次最新的版本号不是数字是尝试对其进行转换,如果无法转换则直接置零
307
+ if (oldVersion.match('\D')) oldVersion = 0
308
+ else oldVersion = Number.parseInt(oldVersion)
309
+ }
310
+ // 存储本次更新的内容
311
+ const newInfo = {
312
+ version: oldVersion + 1,
313
+ change: expand?.change ?? []
314
+ }
315
+ // 整理更新的数据
316
+ const tidied = tidyDiff(dif, expand)
317
+ if (expand?.all) return writeJson({
318
+ global: (oldUpdate?.global ?? 0) + (tidied.updateGlobal ? 1 : 0),
319
+ info: [newInfo]
320
+ })
321
+ // 如果没有更新的文件就直接退出
322
+ if (
323
+ tidied.page.size === 0 && tidied.file.size === 0 &&
324
+ !(tidied.archives || tidied.categories || tidied.tags || tidied.index)
325
+ ) return writeJson(oldUpdate ?? {
326
+ global: 0,
327
+ info: [{version: 0}]
328
+ })
329
+ pushUpdateToInfo(newInfo, tidied)
330
+ const result = mergeUpdateWithOld(newInfo, oldUpdate, tidied)
331
+ return writeJson(result)
332
+ }
333
+
334
+ const mergeUpdateWithOld = (newInfo, oldUpdate, tidied) => {
335
+ const result = {
336
+ global: (oldUpdate?.global ?? 0) + (tidied.updateGlobal ? 1 : 0),
337
+ info: [newInfo]
338
+ }
339
+ // noinspection JSUnresolvedVariable
340
+ const charLimit = pluginConfig.json.charLimit ?? 1024
341
+ if (JSON.stringify(result).length > charLimit) {
342
+ return {
343
+ global: result.global,
344
+ info: [{version: newInfo.version}]
345
+ }
346
+ }
347
+ if (!oldUpdate) return result
348
+ for (let it of oldUpdate.info) {
349
+ if (it.change) it.change = zipInfo(newInfo, it)
350
+ result.info.push(it)
351
+ if (JSON.stringify(result).length > charLimit) {
352
+ result.info.pop()
353
+ break
354
+ }
355
+ }
356
+ return result
357
+ }
358
+
359
+ // 压缩相同项目
360
+ const zipInfo = (newInfo, oldInfo) => {
361
+ oldInfo = oldInfo.change
362
+ newInfo = newInfo.change
363
+ const result = []
364
+ for (let i = oldInfo.length - 1; i !== -1; --i) {
365
+ const value = oldInfo[i]
366
+ if (value.flag === 'page' && newInfo.find(it => it.flag === 'html')) continue
367
+ const newValue = newInfo.find(it => it.flag === value.flag)
368
+ if (!newValue) {
369
+ result.push(value)
370
+ continue
371
+ }
372
+ if (!value.value) continue
373
+ const isArray = Array.isArray(newValue.value)
374
+ if (Array.isArray(value.value)) {
375
+ const array = value.value
376
+ .filter(it => isArray ? !newValue.value.find(that => that === it) : it !== newValue.value)
377
+ if (array.length === 0) continue
378
+ result.push({flag: value.flag, value: array.length === 1 ? array[0] : array})
379
+ } else if (isArray) {
380
+ if (!newValue.value.find(it => it === value.value))
381
+ result.push(value)
382
+ } else {
383
+ if (newValue.value !== value.value)
384
+ result.push(value)
385
+ }
386
+ }
387
+ return result.length === 0 ? undefined : result
388
+ }
389
+
390
+ // 将更新推送到 info
391
+ const pushUpdateToInfo = (info, tidied) => {
392
+ const merges = [] // 要合并的更新
393
+ // 推送页面更新
394
+ // noinspection JSUnresolvedVariable
395
+ if (tidied.page.size > (pluginConfig.json.maxHtml ?? 15)) {
396
+ // 如果 html 数量超过阈值就直接清掉所有 html
397
+ info.change.push({flag: 'html'})
398
+ } else {
399
+ const pages = [] // 独立更新
400
+ tidied.page.forEach(it => pages.push(it))
401
+ const {tag_dir, archive_dir, category_dir} = config
402
+ if (tidied.tags) merges.push(tag_dir)
403
+ if (tidied.archives) merges.push(archive_dir)
404
+ if (tidied.categories) merges.push(category_dir)
405
+ if (tidied.index) {
406
+ pages.push(clipPageName(root, false))
407
+ merges.push('page')
408
+ }
409
+ if (pages.length > 0)
410
+ info.change.push({flag: 'page', value: pages})
411
+ }
412
+ for (let key in tidied.custom) merges.push(key)
413
+ if (merges.length > 0)
414
+ info.change.push({flag: 'str', value: merges.map(it => `/${it}/`)})
415
+ // 推送文件更新
416
+ if (tidied.file.size > 0) {
417
+ const list = []
418
+ tidied.file.forEach(it => list.push(it))
419
+ info.change.push({flag: 'file', value: list})
420
+ }
421
+ }
422
+
423
+ // 将 diff 整理分类
424
+ const tidyDiff = (dif, expand) => {
425
+ const tidied = {
426
+ /** 所有 HTML 页面 */
427
+ page: new Set(),
428
+ /** 所有文件 */
429
+ file: new Set(),
430
+ /** 标记 tags 是否更新 */
431
+ tags: false,
432
+ /** 标记 archives 是否更新 */
433
+ archives: false,
434
+ /** 标记 categories 是否更新 */
435
+ categories: false,
436
+ /** 标记 index 是否更新 */
437
+ index: false,
438
+ /** 自定义配置项 */
439
+ custom: {},
440
+ /** 标记是否更新 global 版本号 */
441
+ updateGlobal: expand?.global
442
+ }
443
+ // noinspection JSUnresolvedVariable
444
+ const mode = pluginConfig.json.precisionMode
445
+ for (let it of dif) {
446
+ const url = new URL(nodePath.join(root, it)) // 当前文件的 URL
447
+ const cache = findCache(url) // 查询缓存
448
+ if (!cache) {
449
+ logger.error(`[buildUpdate] 指定 URL(${url.pathname}) 未查询到缓存规则!`)
450
+ continue
451
+ }
452
+ if (!cache.clean) tidied.updateGlobal = true
453
+ if (isMerge(url.pathname, tidied)) continue
454
+ if (it.match(/(\/|\.html)$/)) { // 判断缓存是否是 html
455
+ if (mode.html ?? false) tidied.page.add(url.pathname)
456
+ else tidied.page.add(clipPageName(url.href, !it.endsWith('/')))
457
+ } else {
458
+ const extendedName = (it.includes('.') ? it.match(/[^.]+$/)[0] : null) ?? 'default'
459
+ const setting = mode[extendedName] ?? (mode.default ?? false)
460
+ if (setting) tidied.file.add(url.pathname)
461
+ else {
462
+ let name = url.href.match(/[^/]+$/)[0]
463
+ if (!name) throw `${url.href} 格式错误`
464
+ tidied.file.add(name)
465
+ }
466
+ }
467
+ }
468
+ return tidied
469
+ }
470
+
471
+ function findCache(url) {
472
+ url = new URL(replaceRequest(url.href))
473
+ for (let key in cacheList) {
474
+ const value = cacheList[key]
475
+ if (value.match(url)) return value
476
+ }
477
+ return null
478
+ }
479
+
480
+ function replaceRequest(url) {
481
+ if (!modifyRequest) return url
482
+ const request = new Request(url)
483
+ const newRequest = modifyRequest(request)
484
+ return newRequest?.url ?? url
485
+ }
486
+
487
+ function replaceDevRequest(url) {
488
+ const external = pluginConfig.external
489
+ if (!external?.enable || !external.replace) return url
490
+ for (let value of external.replace) {
491
+ for (let source of value.source) {
492
+ if (url.match(source)) {
493
+ // noinspection JSUnresolvedVariable
494
+ url = url.replace(source, value.dist)
495
+ }
496
+ }
497
+ }
498
+ return url
499
+ }
500
+ }
@@ -129,14 +129,17 @@
129
129
  })
130
130
  }
131
131
  return fetchFile(new Request('/update.json'), false)
132
- .then(response => response.json())
133
- .then(json =>
134
- parseJson(json).then(result =>
135
- result.list ? deleteCache(result.list).then(list => {
136
- return {list, version: result.version}
137
- }) : {version: result}
138
- )
139
- )
132
+ .then(response => {
133
+ if (checkResponse(response))
134
+ return response.json().then(json =>
135
+ parseJson(json).then(result =>
136
+ result.list ? deleteCache(result.list).then(list => {
137
+ return {list, version: result.version}
138
+ }) : {version: result}
139
+ )
140
+ )
141
+ else console.error(`拉取 update.json 时出现 ${response.status} 异常`)
142
+ })
140
143
  }
141
144
 
142
145
  /**
@@ -0,0 +1,160 @@
1
+ module.exports = (hexo, config, pluginConfig, rules) => {
2
+ const {
3
+ modifyRequest,
4
+ fetchNoCache,
5
+ getCdnList,
6
+ getSpareUrls,
7
+ blockRequest
8
+ } = rules
9
+ const nodePath = require('path')
10
+ const fs = require('fs')
11
+
12
+ const root = config.url + (config.root ?? '/')
13
+
14
+ // noinspection JSUnresolvedVariable
15
+ hexo.extend.generator.register('buildSw', () => {
16
+ // noinspection JSUnresolvedVariable
17
+ if (pluginConfig.sw.custom) return
18
+ const absPath = module.path + '/sw-template.js'
19
+ const rootPath = nodePath.resolve('./')
20
+ const relativePath = nodePath.relative(rootPath, absPath)
21
+ // 获取拓展文件
22
+ let cache = fs.readFileSync('sw-rules.js', 'utf8')
23
+ .replaceAll('module.exports.', 'const ')
24
+ if (!fetchNoCache) {
25
+ // noinspection JSUnresolvedVariable
26
+ if (pluginConfig.sw.cdnRacing && getCdnList) {
27
+ cache +=`
28
+ const fetchFile = (request, banCache) => {
29
+ const fetchArgs = {
30
+ cache: banCache ? 'no-store' : 'default',
31
+ mode: 'cors',
32
+ credentials: 'same-origin'
33
+ }
34
+ const list = getCdnList(request.url)
35
+ if (!list || !Promise.any) return fetch(request, fetchArgs)
36
+ const res = list.map(url => new Request(url, request))
37
+ const controllers = []
38
+ return Promise.any(res.map(
39
+ (it, index) => fetch(it, Object.assign(
40
+ {signal: (controllers[index] = new AbortController()).signal},
41
+ fetchArgs
42
+ )).then(response => checkResponse(response) ? {index, response} : Promise.reject())
43
+ )).then(it => {
44
+ for (let i in controllers) {
45
+ if (i != it.index) controllers[i].abort()
46
+ }
47
+ return it.response
48
+ })
49
+ }
50
+ `
51
+ } else { // noinspection JSUnresolvedVariable
52
+ if (pluginConfig.sw.spareUrl && getSpareUrls) {
53
+ cache += `
54
+ const fetchFile = (request, banCache, spare = null) => {
55
+ const fetchArgs = {
56
+ cache: banCache ? 'no-store' : 'default',
57
+ mode: 'cors',
58
+ credentials: 'same-origin'
59
+ }
60
+ if (!spare) spare = getSpareUrls(request.url)
61
+ if (!spare) return fetch(request, fetchArgs)
62
+ const list = spare.list
63
+ const controllers = []
64
+ let error = 0
65
+ return new Promise((resolve, reject) => {
66
+ const pull = () => {
67
+ const flag = controllers.length
68
+ if (flag === list.length) return
69
+ const plusError = () => {
70
+ if (++error === list.length) reject(\`请求 \${request.url} 失败\`)
71
+ else if (flag + 1 === controllers.length) {
72
+ clearTimeout(controllers[flag].id)
73
+ pull()
74
+ }
75
+ }
76
+ controllers.push({
77
+ ctrl: new AbortController(),
78
+ id: setTimeout(pull, spare.timeout)
79
+ })
80
+ fetch(new Request(list[flag], request), fetchArgs).then(response => {
81
+ if (checkResponse(response)) {
82
+ for (let i in controllers) {
83
+ if (i !== flag) controllers[i].ctrl.abort()
84
+ }
85
+ clearTimeout(controllers[controllers.length - 1].id)
86
+ resolve(response)
87
+ } else plusError()
88
+ }).catch(plusError)
89
+ }
90
+ pull()
91
+ })
92
+ }
93
+ `
94
+ } else cache += `
95
+ const fetchFile = (request, banCache) => fetch(request, {
96
+ cache: banCache ? "no-store" : "default",
97
+ mode: 'cors',
98
+ credentials: 'same-origin'
99
+ })
100
+ `
101
+ }
102
+ }
103
+ if (!getSpareUrls) cache += `\nconst getSpareUrls = _ => {}`
104
+ // noinspection JSUnresolvedVariable
105
+ let swContent = fs.readFileSync(relativePath, 'utf8')
106
+ .replaceAll("const { cacheList, fetchFile, getSpareUrls } = require('../sw-rules')", cache)
107
+ .replaceAll("'@$$[escape]'", (pluginConfig.sw.escape ?? 0).toString())
108
+ .replaceAll("'@$$[cacheName]'", `'${pluginConfig.sw.cacheName ?? 'kmarBlogCache'}'`)
109
+ if (modifyRequest) {
110
+ swContent = swContent.replaceAll('// [modifyRequest call]', `
111
+ const modify = modifyRequest(request)
112
+ if (modify) request = modify
113
+ `).replaceAll('// [modifyRequest else-if]', `
114
+ else if (modify) event.respondWith(fetch(request))
115
+ `)
116
+ }
117
+ if (blockRequest) {
118
+ swContent = swContent.replace('// [blockRequest call]', `
119
+ if (blockRequest(url))
120
+ return event.respondWith(new Response(null, {status: 208}))
121
+ `)
122
+ }
123
+ return {
124
+ path: 'sw.js',
125
+ data: swContent
126
+ }
127
+ })
128
+
129
+ // 生成注册 sw 的代码
130
+ // noinspection JSUnresolvedVariable
131
+ hexo.extend.injector.register('head_begin', () =>
132
+ `<script>
133
+ (() => {
134
+ const sw = navigator.serviceWorker
135
+ const error = () => ${pluginConfig.sw.onerror}
136
+ if (!sw?.register('${new URL(root).pathname}sw.js')?.then(() => {
137
+ if (!sw.controller) ${pluginConfig.sw.onsuccess}
138
+ })?.catch(error)) error()
139
+ })()
140
+ </script>`,
141
+ "default")
142
+
143
+ // 插入 sw-dom.js
144
+ if (!pluginConfig.dom?.custom) {
145
+ // noinspection JSUnresolvedVariable,HtmlUnknownTarget
146
+ hexo.extend.injector.register('body_begin', () => `<script src="/sw-dom.js"></script>`)
147
+ // noinspection JSUnresolvedVariable
148
+ hexo.extend.generator.register('buildDomJs', () => {
149
+ const absPath = module.path + '/sw-dom.js'
150
+ const rootPath = nodePath.resolve('./')
151
+ const relativePath = nodePath.relative(rootPath, absPath)
152
+ const template = fs.readFileSync(relativePath, 'utf-8')
153
+ .replaceAll('// ${onSuccess}', pluginConfig.dom.onsuccess)
154
+ return {
155
+ path: 'sw-dom.js',
156
+ data: template
157
+ }
158
+ })
159
+ }
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-swpp",
3
- "version": "2.3.0-beta.1",
3
+ "version": "2.3.0",
4
4
  "main": "index.js",
5
5
  "dependencies": {
6
6
  "hexo-log": "^3.0.0",
@@ -12,8 +12,8 @@
12
12
  "index.js",
13
13
  "lib/sw-template.js",
14
14
  "lib/sw-dom.js",
15
- "lib/sw.js",
16
- "lib/json.js",
15
+ "lib/swBuilder.js",
16
+ "lib/jsonBuilder.js",
17
17
  "lib/sort.js"
18
18
  ],
19
19
  "keywords": [