swpp-backends 0.0.1-alpha.0 → 0.0.2-alpha

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,417 +0,0 @@
1
- import fs from 'fs'
2
- import nodePath from 'path'
3
- import {Request} from 'node-fetch'
4
- import {readRules} from './SwppRules'
5
- import * as crypto from 'crypto'
6
- import {Buffer} from 'buffer'
7
- import HTMLParser from 'fast-html-parser'
8
- import CSSParser from 'css'
9
- import {fetchFile, readEjectData} from './Utils'
10
-
11
- /**
12
- * 版本信息(可以用 JSON 序列化)
13
- * @see VersionMap
14
- */
15
- export interface VersionJson {
16
- version: number,
17
- list: VersionMap
18
- }
19
-
20
- /**
21
- * 版本列表
22
- *
23
- * + key 为文件的 URL
24
- * + value {string} 为 URL 对应文件的 md5 值
25
- * + value {string[]} 为 stable 文件其中包含的 URL
26
- */
27
- export interface VersionMap {
28
- [propName: string]: any
29
- }
30
-
31
- /**
32
- * 遍历指定目录及其子目录中包含的所有文件(不遍历文件夹)
33
- * @param root 根目录
34
- * @param cb 回调函数(接收的参数是文件的相对路径)
35
- */
36
- function eachAllFile(root: string, cb: (path: string) => void) {
37
- const stats = fs.statSync(root)
38
- if (stats.isFile()) cb(root)
39
- else {
40
- const files = fs.readdirSync(root)
41
- files.forEach(it => eachAllFile(nodePath.join(root, it), cb))
42
- }
43
- }
44
-
45
- /**
46
- * 判断指定 URL 是否排除
47
- *
48
- * + **执行该函数前必须调用过 [loadRules]**
49
- *
50
- * @param webRoot 网站域名
51
- * @param url 要判断的 URL
52
- */
53
- export function isExclude(webRoot: string, url: string): boolean {
54
- const exclude = readRules().config?.json?.exclude
55
- if (!exclude) throw 'exclude 为空'
56
- const list = isExternalLink(webRoot, url) ? exclude.other : exclude.localhost
57
- for (let reg of list) {
58
- if (url.match(reg)) return true
59
- }
60
- return false
61
- }
62
-
63
- /**
64
- * 判断指定 URL 是否是 stable 的
65
- *
66
- * + **执行该函数前必须调用过 [loadRules]**
67
- */
68
- export function isStable(url: string): boolean {
69
- const stable = readRules().config?.external?.stable
70
- if (!stable) throw 'stable 为空'
71
- for (let reg of stable) {
72
- if (url.match(reg)) return true
73
- }
74
- return false
75
- }
76
-
77
- /**
78
- * 从指定 URL 加载 cache json
79
- *
80
- * + **执行该函数前必须调用过 [loadRules]**
81
- */
82
- export async function loadVersionJson(url: string): Promise<VersionJson> {
83
- const response = await fetchFile(url)
84
- return _oldVersionJson = (await response.json()) as VersionJson
85
- }
86
-
87
- let _oldVersionJson: VersionJson
88
- let _newVersionJson: VersionJson
89
- let _mergeVersionMap: VersionMap
90
-
91
- /**
92
- * 读取最后一次加载的 version json
93
- *
94
- * + **执行该函数前必须调用过 [loadRules]**
95
- * + **调用该函数前必须调用过 [loadCacheJson]**
96
- */
97
- export function readOldVersionJson(): VersionJson {
98
- if (!_oldVersionJson) throw 'cache json 尚未初始化'
99
- return _oldVersionJson
100
- }
101
-
102
- /**
103
- * 读取最后一次构建的 VersionJson
104
- *
105
- * + **执行该函数前必须调用过 [loadRules]**
106
- * + **调用该函数前必须调用过 [loadCacheJson]**
107
- * + **执行该函数前必须调用过 [buildVersionJson]**
108
- * + **执行该函数前必须调用过 [calcEjectValues]**
109
- */
110
- export function readNewVersionJson(): VersionJson {
111
- if (!_newVersionJson) throw 'cache json 尚未初始化'
112
- return _newVersionJson
113
- }
114
-
115
- /**
116
- * 读取新旧版本文件合并后的版本地图
117
- *
118
- * + **执行该函数前必须调用过 [loadRules]**
119
- * + **调用该函数前必须调用过 [loadCacheJson]**
120
- * + **执行该函数前必须调用过 [buildVersionJson]**
121
- * + **执行该函数前必须调用过 [calcEjectValues]**
122
- */
123
- export function readMergeVersionMap(): VersionMap {
124
- if (_mergeVersionMap) return _mergeVersionMap
125
- const map: VersionMap = {}
126
- Object.assign(map, readOldVersionJson().list)
127
- Object.assign(map, readNewVersionJson().list)
128
- return _mergeVersionMap = map
129
- }
130
-
131
- /**
132
- * 构建一个 cache json
133
- *
134
- * + **执行该函数前必须调用过 [loadRules]**
135
- * + **调用该函数前必须调用过 [loadCacheJson]**
136
- * + **执行该函数前必须调用过 [calcEjectValues]**
137
- *
138
- * @param protocol 网站的网络协议
139
- * @param webRoot 网站域名(包括二级域名)
140
- * @param root 网页根目录(首页 index.html 所在目录)
141
- */
142
- export async function buildVersionJson(
143
- protocol: ('https://' | 'http://'), webRoot: string, root: string
144
- ): Promise<VersionJson> {
145
- const list: VersionMap = {}
146
- eachAllFile(root, async path => {
147
- const endIndex = path.length - (path.endsWith('/index.html') ? 10 : 0)
148
- const url = new URL(protocol + nodePath.join(webRoot, path.substring(root.length, endIndex)))
149
- const pathname = url.pathname
150
- if (isExclude(webRoot, pathname)) return
151
- let content = null
152
- if (findCache(url)) {
153
- content = fs.readFileSync(path, 'utf-8')
154
- const key = decodeURIComponent(url.pathname)
155
- list[key] = crypto.createHash('md5').update(content).digest('hex')
156
- }
157
- if (pathname.endsWith('/') || pathname.endsWith('.html')) {
158
- if (!content) content = fs.readFileSync(path, 'utf-8')
159
- await eachAllLinkInHtml(webRoot, content, list)
160
- } else if (pathname.endsWith('.css')) {
161
- if (!content) content = fs.readFileSync(path, 'utf-8')
162
- await eachAllLinkInCss(webRoot, content, list)
163
- } else if (pathname.endsWith('.js')) {
164
- if (!content) content = fs.readFileSync(path, 'utf-8')
165
- await eachAllLinkInJavaScript(webRoot, content, list)
166
- }
167
- })
168
- return _newVersionJson = {
169
- version: 3, list
170
- }
171
- }
172
-
173
- /**
174
- * 检索一个 URL 指向的文件中所有地外部链接
175
- *
176
- * 该函数会处理该 URL 指向的文件和文件中直接或间接包含的所有 URL
177
- *
178
- * + **执行该函数前必须调用过 [loadRules]**
179
- * + **调用该函数前必须调用过 [loadCacheJson]**
180
- * + **执行该函数前必须调用过 [calcEjectValues]**
181
- *
182
- * @param webRoot 网站域名
183
- * @param url 要检索的 URL
184
- * @param result 存放结果的对象
185
- * @param event 检索到一个 URL 时触发的事件
186
- */
187
- export async function eachAllLinkInUrl(
188
- webRoot: string, url: string, result: VersionMap, event?: (url: string) => void
189
- ) {
190
- if (url.startsWith('//')) url = 'http' + url
191
- if (url in result) return event?.(url)
192
- if (!url.startsWith('http') || isExclude(webRoot, url)) return
193
- if (!(isExternalLink(webRoot, url) && findCache(new URL(url)))) return
194
- event?.(url)
195
- const stable = isStable(url)
196
- if (stable) {
197
- const old = readOldVersionJson().list
198
- if (url in old) {
199
- const copyTree = (key: string) => {
200
- const value = old[key]
201
- if (!value) return
202
- result[key] = value
203
- if (Array.isArray(value)) {
204
- result[key] = value
205
- for (let url of value) {
206
- copyTree(url)
207
- }
208
- } else {
209
- event?.(value)
210
- }
211
- }
212
- copyTree(url)
213
- return
214
- }
215
- }
216
- const response = await fetchFile(url)
217
- if (![200, 301, 302, 307, 308].includes(response.status))
218
- throw response
219
- const pathname = new URL(url).pathname
220
- let content: string | undefined
221
- const relay: string[] = []
222
- const nextEvent = (it: string) => {
223
- relay.push(it)
224
- event?.(it)
225
- }
226
- switch (true) {
227
- case pathname.endsWith('.html'): case pathname.endsWith('/'):
228
- content = await response.text()
229
- await eachAllLinkInHtml(webRoot, content, result, nextEvent)
230
- break
231
- case pathname.endsWith('.css'):
232
- content = await response.text()
233
- await eachAllLinkInCss(webRoot, content, result, nextEvent)
234
- break
235
- case pathname.endsWith('.js'):
236
- content = await response.text()
237
- await eachAllLinkInJavaScript(webRoot, content, result, nextEvent)
238
- break
239
- default:
240
- if (stable) {
241
- result[url] = []
242
- } else {
243
- const buffer = Buffer.from(await response.arrayBuffer())
244
- result[url] = crypto.createHash('md5').update(buffer).digest('hex')
245
- }
246
- break
247
- }
248
- if (content) {
249
- if (stable) {
250
- result[url] = relay
251
- } else {
252
- result[url] = crypto.createHash('md5').update(content).digest('hex')
253
- }
254
- }
255
- }
256
-
257
- /**
258
- * 检索 HTML 文件中的所有外部链接
259
- *
260
- * 该函数仅处理 HTML 当中直接或间接包含的 URL,不处理文件本身
261
- *
262
- * + **执行该函数前必须调用过 [loadRules]**
263
- * + **调用该函数前必须调用过 [loadCacheJson]**
264
- *
265
- * @param webRoot 网站域名
266
- * @param content HTML 文件内容
267
- * @param result 存放结果的对象
268
- * @param event 检索到 URL 时触发的事件
269
- */
270
- export async function eachAllLinkInHtml(
271
- webRoot: string, content: string, result: VersionMap, event?: (url: string) => void
272
- ) {
273
- const each = async (node: HTMLParser.HTMLElement) => {
274
- let url: string | undefined = undefined
275
- switch (node.tagName) {
276
- case 'link':
277
- url = node.attributes.href
278
- break
279
- case 'script': case 'img': case 'source': case 'iframe': case 'embed':
280
- url = node.attributes.src
281
- break
282
- case 'object':
283
- url = node.attributes.data
284
- break
285
- }
286
- if (url) {
287
- await eachAllLinkInUrl(webRoot, url, result, event)
288
- } else if (node.tagName === 'script') {
289
- await eachAllLinkInJavaScript(webRoot, node.rawText, result, event)
290
- } else if (node.tagName === 'style') {
291
- await eachAllLinkInCss(webRoot, node.rawText, result, event)
292
- }
293
- for (let childNode of node.childNodes) {
294
- await each(childNode)
295
- }
296
- }
297
- await each(HTMLParser.parse(content, { style: true, script: true }))
298
- }
299
-
300
- /**
301
- * 检索 CSS 文件中的所有外部链
302
- *
303
- * 该函数仅处理 CSS 当中直接或间接包含的 URL,不处理文件本身
304
- *
305
- * + **执行该函数前必须调用过 [loadRules]**
306
- * + **调用该函数前必须调用过 [loadCacheJson]**
307
- *
308
- * @param webRoot 网站域名
309
- * @param content CSS 文件内容
310
- * @param result 存放结果的对象
311
- * @param event 当检索到一个 URL 后触发的事件
312
- */
313
- export async function eachAllLinkInCss(
314
- webRoot: string, content: string, result: VersionMap, event?: (url: string) => void
315
- ) {
316
- const each = async (any: Array<any> | undefined) => {
317
- if (!any) return
318
- for (let rule of any) {
319
- switch (rule.type) {
320
- case 'rule':
321
- await each(rule.declarations)
322
- break
323
- case 'declaration':
324
- const value: string = rule.value
325
- const list = value.match(/url\(['"]?([^'")]+)['"]?\)/g)
326
- ?.map(it => it.replace(/(^url\(['"])|(['"]\)$)/g, ''))
327
- if (list) {
328
- for (let url of list) {
329
- await eachAllLinkInUrl(webRoot, url, result, event)
330
- }
331
- }
332
- break
333
- case 'import':
334
- const url = rule.import.trim().replace(/^["']|["']$/g, '')
335
- await eachAllLinkInUrl(webRoot, url, result, event)
336
- break
337
- }
338
- }
339
- }
340
- await each(CSSParser.parse(content).stylesheet?.rules)
341
- }
342
-
343
- /**
344
- * 遍历 JS 文件中地所有外部链接
345
- *
346
- * 该函数仅处理 JS 当中直接或间接包含的 URL,不处理文件本身
347
- *
348
- * + **执行该函数前必须调用过 [loadRules]**
349
- * + **调用该函数前必须调用过 [loadCacheJson]**
350
- *
351
- * @param webRoot 网站域名
352
- * @param content JS 文件内容
353
- * @param result 存放结果的对象
354
- * @param event 当检索到一个 URL 后触发的事件
355
- */
356
- export async function eachAllLinkInJavaScript(
357
- webRoot: string, content: string, result: VersionMap, event?: (url: string) => void
358
- ) {
359
- const ruleList = readRules().config?.external?.js
360
- if (!ruleList) throw 'ruleList 为空'
361
- for (let value of ruleList) {
362
- if (typeof value === 'function') {
363
- const urls: string[] = value(content)
364
- for (let url of urls) {
365
- await eachAllLinkInUrl(webRoot, url, result, event)
366
- }
367
- } else {
368
- const {head, tail} = value
369
- const reg = new RegExp(`${head}(['"\`])(.*?)(['"\`])${tail}`, 'mg')
370
- const list = content.match(reg)
371
- ?.map(it => it.substring(head.length, it.length - tail.length).trim())
372
- ?.map(it => it.replace(/^['"`]|['"`]$/g, ''))
373
- if (list) {
374
- for (let url of list) {
375
- await eachAllLinkInUrl(webRoot, url, result, event)
376
- }
377
- }
378
- }
379
- }
380
- }
381
-
382
- /** 判断一个 URL 是否是外部链接 */
383
- function isExternalLink(webRoot: string, url: string): boolean {
384
- return new RegExp(`^(https?:)?\\/\\/${webRoot}`).test(url)
385
- }
386
-
387
- /**
388
- * 查询指定 URL 对应的缓存规则
389
- *
390
- * + **执行该函数前必须调用过 [loadRules]**
391
- * + **执行该函数前必须调用过 [calcEjectValues]**
392
- */
393
- export function findCache(url: URL | string): any | null {
394
- const {cacheRules} = readRules()
395
- const eject = readEjectData()
396
- if (typeof url === 'string') url = new URL(url)
397
- url = new URL(replaceRequest(url.href))
398
- for (let key in cacheRules) {
399
- const value = cacheRules[key]
400
- if (value.match(url, eject.nodeEject)) return value
401
- }
402
- return null
403
- }
404
-
405
- /**
406
- * 替换请求
407
- *
408
- * + **执行该函数前必须调用过 [loadRules]**
409
- * + **执行该函数前必须调用过 [calcEjectValues]**
410
- */
411
- export function replaceRequest(url: string): string {
412
- const rules = readRules()
413
- if (!('modifyRequest' in rules)) return url
414
- const {modifyRequest} = rules
415
- const request = new Request(url)
416
- return modifyRequest?.(request, readEjectData().nodeEject)?.url ?? url
417
- }
@@ -1,156 +0,0 @@
1
- import {ServiceWorkerConfig} from './SwppConfig'
2
- import {readRules} from './SwppRules'
3
- import {getSource, readEjectData} from './Utils'
4
- import fs from 'fs'
5
- import nodePath from 'path'
6
-
7
- /**
8
- * 构建 sw
9
- *
10
- * + **执行该函数前必须调用过 [loadRules]**
11
- * + **执行该函数前必须调用过 [calcEjectValues]**
12
- */
13
- export function buildServiceWorker(): string {
14
- const rules = readRules()
15
- const eject = readEjectData()
16
- const {
17
- modifyRequest,
18
- fetchFile,
19
- getRaceUrls,
20
- getSpareUrls,
21
- blockRequest,
22
- config
23
- } = rules
24
- const serviceWorkerConfig = config.serviceWorker as ServiceWorkerConfig
25
- const templatePath = nodePath.resolve('./', module.path, 'sw-template.js')
26
- // 获取拓展文件
27
- let cache = getSource(rules, undefined, [
28
- 'cacheList', 'modifyRequest', 'getCdnList', 'getSpareUrls', 'blockRequest', 'fetchFile',
29
- ...('external' in rules && Array.isArray(rules.external) ? rules.external : [])
30
- ], true) + '\n'
31
- if (!fetchFile) {
32
- if (getRaceUrls)
33
- cache += JS_CODE_GET_CDN_LIST
34
- else if (getSpareUrls)
35
- cache += JS_CODE_GET_SPARE_URLS
36
- else
37
- cache += JS_CODE_DEF_FETCH_FILE
38
- }
39
- if (!getSpareUrls) cache += `\nconst getSpareUrls = _ => {}`
40
- if ('afterJoin' in rules)
41
- cache += `(${getSource(rules['afterJoin'])})()\n`
42
- if ('afterTheme' in rules)
43
- cache += `(${getSource(rules['afterTheme'])})()\n`
44
- const keyword = "const { cacheList, fetchFile, getSpareUrls } = require('../sw-rules')"
45
- // noinspection JSUnresolvedVariable
46
- let content = fs.readFileSync(templatePath, 'utf8')
47
- .replaceAll("// [insertion site] values", eject.strValue ?? '')
48
- .replaceAll(keyword, cache)
49
- .replaceAll("'@$$[escape]'", (serviceWorkerConfig.escape).toString())
50
- .replaceAll("'@$$[cacheName]'", `'${serviceWorkerConfig.cacheName}'`)
51
- if (modifyRequest) {
52
- content = content.replaceAll('// [modifyRequest call]', `
53
- const modify = modifyRequest(request)
54
- if (modify) request = modify
55
- `).replaceAll('// [modifyRequest else-if]', `
56
- else if (modify) event.respondWith(fetch(request))
57
- `)
58
- }
59
- if (blockRequest) {
60
- content = content.replace('// [blockRequest call]', `
61
- if (blockRequest(url))
62
- return event.respondWith(new Response(null, {status: 208}))
63
- `)
64
- }
65
- // noinspection JSUnresolvedVariable
66
- if (serviceWorkerConfig.debug) {
67
- content = content.replaceAll('// [debug delete]', `
68
- console.debug(\`delete cache: \${url}\`)
69
- `).replaceAll('// [debug put]', `
70
- console.debug(\`put cache: \${key}\`)
71
- `).replaceAll('// [debug message]', `
72
- console.debug(\`receive: \${event.data}\`)
73
- `).replaceAll('// [debug escape]', `
74
- console.debug(\`escape: \${aid}\`)
75
- `)
76
- }
77
- return content
78
- }
79
-
80
- // 缺省的 fetchFile 函数的代码
81
- const JS_CODE_DEF_FETCH_FILE = `
82
- const fetchFile = (request, banCache) => fetch(request, {
83
- cache: banCache ? "no-store" : "default",
84
- mode: 'cors',
85
- credentials: 'same-origin'
86
- })
87
- `
88
-
89
- // getCdnList 函数的代码
90
- const JS_CODE_GET_CDN_LIST = `
91
- const fetchFile = (request, banCache) => {
92
- const fetchArgs = {
93
- cache: banCache ? 'no-store' : 'default',
94
- mode: 'cors',
95
- credentials: 'same-origin'
96
- }
97
- const list = getCdnList(request.url)
98
- if (!list || !Promise.any) return fetch(request, fetchArgs)
99
- const res = list.map(url => new Request(url, request))
100
- const controllers = []
101
- return Promise.any(res.map(
102
- (it, index) => fetch(it, Object.assign(
103
- {signal: (controllers[index] = new AbortController()).signal},
104
- fetchArgs
105
- )).then(response => checkResponse(response) ? {index, response} : Promise.reject())
106
- )).then(it => {
107
- for (let i in controllers) {
108
- if (i != it.index) controllers[i].abort()
109
- }
110
- return it.response
111
- })
112
- }
113
- `
114
-
115
- // getSpareUrls 函数的代码
116
- const JS_CODE_GET_SPARE_URLS = `
117
- const fetchFile = (request, banCache, spare = null) => {
118
- const fetchArgs = {
119
- cache: banCache ? 'no-store' : 'default',
120
- mode: 'cors',
121
- credentials: 'same-origin'
122
- }
123
- if (!spare) spare = getSpareUrls(request.url)
124
- if (!spare) return fetch(request, fetchArgs)
125
- const list = spare.list
126
- const controllers = []
127
- let error = 0
128
- return new Promise((resolve, reject) => {
129
- const pull = () => {
130
- const flag = controllers.length
131
- if (flag === list.length) return
132
- const plusError = () => {
133
- if (++error === list.length) reject(\`请求 \${request.url} 失败\`)
134
- else if (flag + 1 === controllers.length) {
135
- clearTimeout(controllers[flag].id)
136
- pull()
137
- }
138
- }
139
- controllers.push({
140
- ctrl: new AbortController(),
141
- id: setTimeout(pull, spare.timeout)
142
- })
143
- fetch(new Request(list[flag], request), fetchArgs).then(response => {
144
- if (checkResponse(response)) {
145
- for (let i in controllers) {
146
- if (i !== flag) controllers[i].ctrl.abort()
147
- clearTimeout(controllers[i].id)
148
- }
149
- resolve(response)
150
- } else plusError()
151
- }).catch(plusError)
152
- }
153
- pull()
154
- })
155
- }
156
- `