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.
package/src/SwppRules.ts DELETED
@@ -1,200 +0,0 @@
1
- import fs from 'fs'
2
- import {Request, Response} from 'node-fetch'
3
- import nodePath from 'path'
4
- import {SwppConfig, SwppConfigTemplate} from './SwppConfig'
5
-
6
- const defConfig: SwppConfigTemplate = {
7
- serviceWorker: {
8
- escape: 0,
9
- cacheName: 'kmarBlogCache',
10
- debug: false
11
- },
12
- register: {
13
- onerror: () => console.error('Service Worker 注册失败!可能是由于您的浏览器不支持该功能!'),
14
- builder: (root: string, _: any, pluginConfig: SwppConfig) => {
15
- const registerConfig = pluginConfig.register as any
16
- const {onerror, onsuccess} = registerConfig
17
- return `<script>
18
- (() => {
19
- let sw = navigator.serviceWorker
20
- let error = ${onerror.toString()}
21
- if (!sw?.register('${new URL(root).pathname}sw.js')
22
- ${onsuccess ? '?.then(' + onsuccess.toString() + ')' : ''}
23
- ?.catch(error)
24
- ) error()
25
- })()
26
- </script>`
27
- }
28
- },
29
- dom: {},
30
- json: {
31
- maxHtml: 15,
32
- charLimit: 1024,
33
- merge: [],
34
- exclude: {
35
- localhost: [/^\/sw\.js$/],
36
- other: []
37
- }
38
- },
39
- external: {
40
- timeout: 5000,
41
- js: [],
42
- stable: [],
43
- replacer: it => it
44
- }
45
- }
46
-
47
- const eventList: ((rules: any) => void)[] = []
48
- let _rules: SwppRules
49
-
50
- /**
51
- * 读取最后一次构建的 rules
52
- *
53
- * **执行该函数前必须调用过 [loadRules]**
54
- */
55
- export function readRules(): SwppRules {
56
- if (!_rules) throw 'rules 尚未初始化'
57
- return _rules
58
- }
59
-
60
- /**
61
- * 添加一个 rules 映射事件,这个事件允许用户修改 rules 的内容
62
- *
63
- * 执行时按照注册的顺序执行
64
- */
65
- export function addRulesMapEvent(mapper: (rules: any) => void) {
66
- eventList.push(mapper)
67
- }
68
-
69
- /**
70
- * 加载 rules 文件
71
- * @param root 项目根目录
72
- * @param fileName rules 文件名称
73
- * @param selects 附加的可选目录,优先级低于 [root]
74
- */
75
- export function loadRules(root: string, fileName: string, selects: string[]): SwppRules {
76
- // 支持的拓展名
77
- const extensions = ['cjs', 'js']
78
- // 根目录下的 rules 文件
79
- const rootPath = extensions.map(it => nodePath.resolve(root, `${fileName}.${it}`))
80
- .find(it => fs.existsSync(it))
81
- // 其它可选目录下的 rules 文件
82
- const selectPath = selects.flatMap(
83
- value => extensions.map(it => nodePath.resolve(value, `${fileName}.${it}`))
84
- ).find(it => fs.existsSync(it))
85
- if (!(rootPath || selectPath))
86
- throw '未查询到 rules 文件'
87
- const rootRules = rootPath ? { ...require(rootPath) } : {}
88
- const selectRules = selectPath ? require(selectPath) : {}
89
- const config = rootRules.config ?? {}
90
- mergeConfig(config, selectRules.config ?? {})
91
- mergeConfig(config, defConfig)
92
- Object.assign(rootRules, selectRules)
93
- rootRules.config = config
94
- for (let event of eventList) {
95
- event(rootRules)
96
- }
97
- return _rules = rootRules
98
- }
99
-
100
- /** 合并配置项 */
101
- function mergeConfig(dist: any, that: any): any {
102
- for (let key in that) {
103
- const distValue = dist[key]
104
- const thatValue = that[key]
105
- if (!thatValue) continue
106
- switch (typeof distValue) {
107
- case "undefined":
108
- dist[key] = thatValue
109
- break
110
- case "object":
111
- mergeConfig(distValue, thatValue)
112
- break
113
- }
114
- }
115
- return dist
116
- }
117
-
118
- export interface SwppRules {
119
- /** 配置项 */
120
- config: SwppConfig,
121
- /** 缓存规则 */
122
- cacheRules?: {
123
- [propName: string]: CacheRules
124
- },
125
- /**
126
- * 修改 Request
127
- * @param request 原始 request
128
- * @param $eject 用于访问通过 [ejectValues] 函数插入的变量,变量名必须为 `$eject`
129
- * @return 修改后的 Request,不修改的话返回 null 或不返回数据
130
- */
131
- modifyRequest?: (request: Request, $eject: any) => Request | undefined,
132
- /**
133
- * 获取一个 URL 对应的多个 CDN 的 URL
134
- *
135
- * 竞速时除了 URL 外所有参数保持一致
136
- *
137
- * @param url 原始 URL
138
- * @return {?string[]} 返回值不包含则表示去除对原始 URL 地访问。返回 undefined 表示该 URL 不启用竞速
139
- */
140
- getRaceUrls?: (url: string) => string[] | undefined,
141
- /**
142
- * 获取一个 URL 对应的 URL 列表
143
- *
144
- * 访问顺序按列表顺序,所有 URL 访问时参数一致
145
- *
146
- * @param url 原始 URL
147
- * @return {?SpareURLs} 返回 null 或不反悔表示对该 URL 不启用备用 URL 功能
148
- */
149
- getSpareUrls?: (url: string) => SpareURLs | undefined,
150
- /**
151
- * 判断是否阻塞指定响应
152
- * @return {boolean} 返回 true 表示阻塞,false 表示不阻塞
153
- */
154
- blockRequest?: (url: URL) => boolean,
155
- /** 插入到 sw 但不在 node 中执行的代码 */
156
- afterJoin?: VoidFunction,
157
- /** 插入到 sw 但不在 node 中执行的代码,框架主题禁止覆盖该项 */
158
- afterTheme?: VoidFunction,
159
- /** 获取要插入到 sw 中的变量和常量 */
160
- ejectValues?: (framework: any, rules: SwppRules) => {
161
- [propName: string]: EjectValue
162
- },
163
- /** 允许插入到 sw 的值 */
164
- external?: string[],
165
- /**
166
- * 向指定的 request 发起网络请求(GET)
167
- *
168
- * **注意:声明该项后 swpp 内置的“CDN 竞速”“备用 URL”都将失效**
169
- *
170
- * @param request 请求信息
171
- * @param banCache 是否禁用缓存
172
- * @param spare 备用 URL
173
- */
174
- fetchFile?: (request: Request, banCache: boolean, spare?: SpareURLs) => Promise<Response>
175
- }
176
-
177
- export interface CacheRules {
178
- /** 符合该规则的缓存在进行全局清理时是否清除 */
179
- clean: boolean,
180
- /** 是否检查 URL 参数(问号及问号之后的内容) */
181
- search?: boolean,
182
- /**
183
- * 规则匹配器
184
- * @param url 链接的 URL 对象(对象包括 hash 和 search,但禁止使用 hash,search 为 false 或留空时禁止使用 search)
185
- * @param $eject 用于访问通过 [ejectValues] 函数插入的变量,变量名必须为 `$eject`
186
- */
187
- match: (url: URL, $eject?: any) => boolean
188
- }
189
-
190
- export interface SpareURLs {
191
- /** 超时时间 */
192
- timeout: number,
193
- /** URL 列表 */
194
- list: string[]
195
- }
196
-
197
- export interface EjectValue {
198
- prefix: string,
199
- value: string | number | boolean | bigint | object | string[] | number[] | boolean[] | bigint[] | object[]
200
- }
@@ -1,279 +0,0 @@
1
- import {readMergeVersionMap} from './FileAnalyzer'
2
- import {readRules} from './SwppRules'
3
- import {fetchFile, warn} from './Utils'
4
- import {AnalyzerResult} from './VersionAnalyzer'
5
-
6
- let _oldJson: UpdateJson | undefined | null = undefined
7
- const externalChange: ChangeExpression[] = []
8
-
9
- /** 提交修改 */
10
- export function submitChange(...change: ChangeExpression[]) {
11
- externalChange.push(...change)
12
- }
13
-
14
- /**
15
- * 加载版本文件
16
- *
17
- * + **调用该函数前必须调用过 [loadRules]**
18
- */
19
- export async function loadUpdateJson(url: string): Promise<UpdateJson | null> {
20
- if (_oldJson !== undefined) return _oldJson
21
- const response = await fetchFile(url)
22
- if (response.status === 404) {
23
- warn('LoadUpdateJson', `拉取 ${url} 时出现 404 错误,如果您是第一次构建请忽略这个警告。`)
24
- return _oldJson = null
25
- }
26
- return _oldJson = (await response.json()) as UpdateJson
27
- }
28
-
29
- /**
30
- * 读取最后一次加载的版本文件
31
- *
32
- * + **调用该函数前必须调用过 [loadRules]**
33
- * + **调用该函数前必须调用过 [loadUpdateJson]**
34
- */
35
- export function readUpdateJson(): UpdateJson | null {
36
- if (_oldJson === undefined) throw 'UpdateJson 未初始化'
37
- return _oldJson
38
- }
39
-
40
- /**
41
- * 构建新的 update json
42
- *
43
- * + **执行该函数前必须调用过 [loadRules]**
44
- * + **调用该函数前必须调用过 [loadCacheJson]**
45
- * + **执行该函数前必须调用过 [buildVersionJson]**
46
- * + **执行该函数前必须调用过 [calcEjectValues]**
47
- *
48
- * @param root 网站根路径(包括网络协议)
49
- * @param dif 网站文件变化
50
- */
51
- export function buildNewInfo(root: string, dif: AnalyzerResult): UpdateJson {
52
- const config = readRules().config.json
53
- if (!config) throw '功能未开启'
54
- const old = readUpdateJson()
55
- let global = old?.global ?? 0
56
- if (dif.force) return {
57
- global: global + 1,
58
- info: [{
59
- version: old ? old.info[0].version + 1 : 0
60
- }]
61
- }
62
- const change: ChangeExpression[] = []
63
- const info: UpdateVersionInfo = {
64
- version: old ? old.info[0].version + 1 : 0,
65
- change
66
- }
67
- const list = [...dif.refresh, ...dif.deleted, ...dif.variational, ...dif.rules.remove]
68
- const records = {
69
- merge: new Set<string>(),
70
- html: new Set<string>()
71
- }
72
- for (let url of list) {
73
- if (url.startsWith('/')) {
74
- // 本地链接
75
- const merge = config.merge.find(it => url.startsWith(`/${it}/`))
76
- if (merge) {
77
- records.merge.add(merge)
78
- continue
79
- }
80
- url = root + url
81
- }
82
- if (/(\/|\.html)$/.test(url)) { // is html
83
- records.html.add(getShorthand(url, 1))
84
- } else { // not html
85
- change.push({
86
- flag: 'end',
87
- value: getShorthand(url)
88
- })
89
- }
90
- }
91
- if (records.html.size !== 0) {
92
- if (records.html.size > config.maxHtml) {
93
- change.push({flag: 'html'})
94
- } else {
95
- change.push({
96
- flag: 'end',
97
- value: Array.from(records.html)
98
- })
99
- }
100
- }
101
- if (records.merge.size !== 0) {
102
- change.push({
103
- flag: 'begin',
104
- value: Array.from(records.merge).map(it => `/${it}/`)
105
- })
106
- }
107
- change.push(...externalChange)
108
- return zipJson({
109
- global,
110
- info: [info, ...(old?.info ?? [])]
111
- })
112
- }
113
-
114
- function zipJson(json: UpdateJson): UpdateJson {
115
- const record = new Map<FlagStr, string[]>()
116
- /** 合并同名项目 */
117
- function merge(info: UpdateVersionInfo) {
118
- const localRecord = new Map<FlagStr, Set<string>>()
119
- if (!info.change) return
120
- for (let exp of info.change) {
121
- const value = exp.value
122
- if (!localRecord.has(exp.flag))
123
- localRecord.set(exp.flag, new Set())
124
- if (!value) continue
125
- const set = localRecord.get(exp.flag)!
126
- if (typeof value === 'string') {
127
- set.add(value)
128
- } else {
129
- value.forEach(it => set.add(it))
130
- }
131
- }
132
- type FunResult = [FlagStr, string[]]
133
- info.change = Array.from(localRecord)
134
- .map(it => {
135
- if (it[0] === 'html') return [it[0], []] as FunResult
136
- const values = Array.from(it[1])
137
- if (it[0] === 'str' || it[0] === 'reg') return [it[0], values] as FunResult
138
- const filtered = values.filter((value, index) => {
139
- if (it[0] === 'end' && record.has('html') && /(\/|\.html)$/.test(value))
140
- return false
141
- for (let i = 0; i < values.length; i++) {
142
- if (i === index) continue
143
- const that = values[i]
144
- switch (it[0]) {
145
- case 'end':
146
- if (value.endsWith(that)) return false
147
- break
148
- case 'begin':
149
- if (value.startsWith(that)) return false
150
- break
151
- }
152
- }
153
- return true
154
- })
155
- return [it[0], filtered] as FunResult
156
- }).filter(it => it[1].length !== 0 || it[0] === 'html')
157
- .map(it => {
158
- record.set(it[0], it[1])
159
- if (it[1].length === 0) return { flag: it[0] }
160
- return {
161
- flag: it[0],
162
- value: it[1].length === 1 ? it[1][0] : it[1]
163
- }
164
- })
165
- }
166
- /** 移除不可达的表达式 */
167
- function deleteUnreachableExp(list: UpdateVersionInfo[]) {
168
- for (let i = list.length - 1; i > 0; i--) {
169
- const info = list[i]
170
- let change = info.change
171
- if (!change) continue
172
- for (let k = 0; k < change.length; k++) {
173
- const exp = change[k]
174
- const top = record.get(exp.flag)
175
- if (exp.flag === 'html') {
176
- change.splice(k--, 1)
177
- continue
178
- }
179
- let array = typeof exp.value === 'string' ? [exp.value] : exp.value!
180
- const find = (test: (it: string) => boolean) => {
181
- if (!top) return false
182
- return top.find(test)
183
- }
184
- switch (exp.flag) {
185
- case 'end':
186
- array = array.filter(value => {
187
- if (/(\/|\.html)$/.test(value) && record.has('html'))
188
- return false
189
- if (!top) return true
190
- return !find(it => value.endsWith(it))
191
- })
192
- break
193
- case 'begin':
194
- array = array.filter(value => !find(it => value.startsWith(it)))
195
- break
196
- case 'str':
197
- array = array.filter(value => !find(it => value.includes(it)))
198
- break
199
- case 'reg':
200
- array = array.filter(value => !top?.includes(value))
201
- break
202
- }
203
- switch (array.length) {
204
- case 0:
205
- change.splice(k--, 1)
206
- break
207
- case 1:
208
- exp.value = array[0]
209
- break
210
- default:
211
- exp.value = array
212
- break
213
- }
214
- }
215
- if (change.length === 0)
216
- delete info.change
217
- }
218
- }
219
- merge(json.info[0])
220
- deleteUnreachableExp(json.info)
221
- return json
222
- }
223
-
224
- /**
225
- * 获取 URL 的缩写形式
226
- *
227
- * + **执行该函数前必须调用过 [loadRules]**
228
- * + **调用该函数前必须调用过 [loadCacheJson]**
229
- * + **执行该函数前必须调用过 [buildVersionJson]**
230
- * + **执行该函数前必须调用过 [calcEjectValues]**
231
- */
232
- export function getShorthand(url: string, offset: number = 0): string {
233
- const map = readMergeVersionMap()
234
- let collide = new Set<string>()
235
- for (let mapKey in map) {
236
- collide.add(mapKey)
237
- }
238
- let index = Math.max(url.lastIndexOf('/', url.length - offset - 1), url.length - 20)
239
- let result: string
240
- while (true) {
241
- result = url.substring(index)
242
- let count = 0
243
- const removeSet = new Set<string>()
244
- for (let url of collide) {
245
- if (url.endsWith(result)) {
246
- ++count
247
- if (count === 2) break
248
- } else {
249
- removeSet.add(url)
250
- }
251
- }
252
- switch (count) {
253
- case 1: return result
254
- case 2:
255
- --index
256
- removeSet.forEach(it => collide.delete(it))
257
- break
258
- default:
259
- throw '意料之外的错误:' + count
260
- }
261
- }
262
- }
263
-
264
- export interface UpdateJson {
265
- global: number,
266
- info: UpdateVersionInfo[]
267
- }
268
-
269
- export interface UpdateVersionInfo {
270
- version: number,
271
- change?: ChangeExpression[]
272
- }
273
-
274
- export interface ChangeExpression {
275
- flag: FlagStr,
276
- value?: string | string[]
277
- }
278
-
279
- export type FlagStr = 'html' | 'end' | 'begin' | 'str' | 'reg'
package/src/Utils.ts DELETED
@@ -1,195 +0,0 @@
1
- import fetch from 'node-fetch'
2
- import {readRules} from './SwppRules'
3
-
4
- const logger = require('hexo-log').default({
5
- debug: false,
6
- silent: false
7
- })
8
-
9
- export function error(type: string, message: string) {
10
- logger.error(`[SWPP ${type}] ${message}`)
11
- }
12
-
13
- export function warn(type: string, message: string) {
14
- logger.warn(`[SWPP ${type}] ${message}`)
15
- }
16
-
17
- export interface EjectCache {
18
- strValue: string,
19
- nodeEject: any
20
- }
21
-
22
- let ejectData: EjectCache | undefined = undefined
23
-
24
- /**
25
- * 获取 eject values
26
- *
27
- * + **执行该函数前必须调用过 [loadRules]**
28
- *
29
- * @param framework 框架对象
30
- */
31
- export function calcEjectValues(framework: any) {
32
- const rules = readRules()
33
- if (!('ejectValues' in rules)) return
34
- // noinspection JSUnresolvedReference
35
- const eject = rules.ejectValues?.(framework, rules)
36
- const nodeEject: any = {}
37
- let ejectStr = ''
38
- for (let key in eject) {
39
- if (!key.match(/^[A-Za-z0-9]+$/)) {
40
- logger.error(`[SWPP EjectValues] 变量名 [${key}] 仅允许包含英文字母和阿拉伯数字!`)
41
- throw '变量名违规:' + key
42
- }
43
- const data = eject[key]
44
- const str = getSource(data.value, name => {
45
- if (['string', 'number', 'boolean', 'object', 'array', 'bigint'].includes(name))
46
- return true
47
- logger.error(`[SWPP EjectValue] 不支持导出 ${name} 类型的数据`)
48
- throw `不支持的键值:key=${key}, value type=${name}`
49
- })
50
- ejectStr += ` ${data.prefix} ${key} = ${str}\n`
51
- nodeEject[key] = data.value
52
- }
53
- ejectData = {
54
- strValue: ejectStr,
55
- nodeEject
56
- }
57
- }
58
-
59
- /**
60
- * 读取最近的已计算的 eject 数据
61
- *
62
- * + **执行该函数前必须调用过 [loadRules]**
63
- * + **执行该函数前必须调用过 [calcEjectValues]**
64
- */
65
- export function readEjectData(): EjectCache {
66
- if (!ejectData) throw 'eject data 尚未初始化'
67
- return ejectData
68
- }
69
-
70
- /**
71
- * 获取指定值的 js 源码表达形式
72
- * @param obj 要转换的对象
73
- * @param typeChecker 类型检查器,用于筛除不希望映射的类型
74
- * @param whiteList 白名单,当 obj 为 Object 时将只转换在白名单中的值(不会传递)
75
- * @param isTop 是否为顶层元素,为 true 且 obj 为 Object 时将去除最外层的大括号,改为 let(不会传递)
76
- */
77
- export function getSource(
78
- obj: any,
79
- typeChecker: ((name: string) => boolean) | undefined = undefined,
80
- whiteList: string[] | undefined = undefined,
81
- isTop: boolean = false
82
- ): string {
83
- const type = typeof obj
84
- if (typeChecker) {
85
- let value
86
- if (type === 'object') {
87
- value = Array.isArray(obj) ? 'array' : type
88
- } else value = type
89
- if (!typeChecker(value)) return ''
90
- }
91
- switch (type) {
92
- case "undefined": return 'undefined'
93
- case "object":
94
- if (Array.isArray(obj)) {
95
- return '[' + (obj as Array<any>).map(it => getSource(it)).join(', ') + ']'
96
- } else {
97
- let result = isTop ? '' : '{\n'
98
- result += Object.getOwnPropertyNames(obj)
99
- .filter(key => !whiteList || whiteList.includes(key))
100
- .map(key => {
101
- const value = obj[key]
102
- let str = getSource(value, typeChecker)
103
- if (str.length === 0) return ''
104
- if (isTop && whiteList && ['cacheList', 'modifyRequest'].includes(key)) {
105
- str = str
106
- .replace(
107
- /\(\s*(.*?)\s*,\s*\$eject\s*\)/g, "$1"
108
- ) // 去掉箭头函数参数表中的 $eject
109
- .replaceAll(
110
- /\$eject\.(\w+)/g,
111
- (_, match) => `eject${match[0].toUpperCase()}${match.substring(1)}`
112
- ) // 将函数体中的 $eject.xxx 替换为 ejectXxx
113
- }
114
- return isTop ? `let ${key} = ${str}` : `${key}: ${str}`
115
- })
116
- .filter(it => it.length !== 0)
117
- .join(isTop ? '\n' : ',\n')
118
- result += isTop ? '' : '}\n'
119
- return result
120
- }
121
- case "string":
122
- if (!obj.includes("'"))
123
- return `'${obj}'`
124
- else if (!obj.includes('"'))
125
- return `"${obj}"`
126
- else if (!obj.includes('`'))
127
- return `\`${obj}\``
128
- else
129
- return `'${(obj as string).replaceAll("'", "\\'")}'`
130
- case "bigint": return `${obj.toString()}n`
131
- default: return obj.toString()
132
- case "symbol":
133
- logger.error("[SWPP ServiceWorkerBuilder] 不支持写入 symbol 类型,请从 sw-rules.js 中移除相关内容!")
134
- throw '不支持写入 symbol 类型'
135
- }
136
- }
137
-
138
- /**
139
- * 拉取文件
140
- *
141
- * **调用该函数前必须调用过 [loadRules]**
142
- */
143
- export async function fetchFile(link: string) {
144
- const config = readRules().config
145
- const url = replaceDevRequest(link)
146
- const opts = {
147
- headers: {
148
- referer: 'kmar-swpp',
149
- 'User-Agent': 'kmar-swpp'
150
- },
151
- timeout: (config.external as any).timeout
152
- }
153
- try {
154
- if (typeof url === 'string') {
155
- return await fetch(url, opts)
156
- } else {
157
- return await fetchSpeed(url)
158
- }
159
- } catch (err) {
160
- // @ts-ignore
161
- if (err.type === 'request-timeout') {
162
- logger.error(
163
- `[SWPP FetchFile] 拉取文件 [${link}] 时出现了超时错误,如果您构建所在的位置无法访问该资源,` +
164
- "请尝试通过 DevReplace(https://kmar.top/posts/73014407/#4ea71e00)功能解决该问题。"
165
- )
166
- throw 'timeout'
167
- }
168
- throw err
169
- }
170
- }
171
-
172
- /**
173
- * 替换编译期的 URL(CDN 竞速)
174
- *
175
- * **调用该函数前必须调用过 [loadRules]**
176
- */
177
- export function replaceDevRequest(link: string): string[] | string {
178
- const config = readRules().config
179
- return config.external?.replacer(link) ?? link
180
- }
181
-
182
- /** 通过 CDN 竞速的方式拉取文件 */
183
- async function fetchSpeed(list: string[]) {
184
- const controllers: AbortController[] = new Array(list.length)
185
- const result = await Promise.any(
186
- list.map((it, index) => fetch(it, {
187
- signal: (controllers[index] = new AbortController()).signal
188
- }).then(response => [200, 301, 302, 307, 308].includes(response.status) ? {index, response} : Promise.reject())
189
- )
190
- )
191
- for (let i = 0; i < controllers.length; i++) {
192
- if (i !== result.index) controllers[i].abort()
193
- }
194
- return result.response
195
- }