@cloudbase/app 3.0.3 → 3.0.5
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/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +36 -7
- package/dist/cjs/libs/adapter-node/constants.d.ts +33 -0
- package/dist/cjs/libs/adapter-node/constants.js +38 -0
- package/dist/cjs/libs/adapter-node/context.d.ts +13 -0
- package/dist/cjs/libs/adapter-node/context.js +162 -0
- package/dist/cjs/libs/adapter-node/index.d.ts +23 -0
- package/dist/cjs/libs/adapter-node/index.js +124 -0
- package/dist/cjs/libs/adapter-node/metadata.d.ts +14 -0
- package/dist/cjs/libs/adapter-node/metadata.js +130 -0
- package/dist/cjs/libs/adapter-node/request.d.ts +38 -0
- package/dist/cjs/libs/adapter-node/request.js +432 -0
- package/dist/cjs/libs/adapter-node/tool.d.ts +2 -0
- package/dist/cjs/libs/adapter-node/tool.js +227 -0
- package/dist/cjs/libs/adapter-node/types.d.ts +78 -0
- package/dist/cjs/libs/adapter-node/types.js +3 -0
- package/dist/cjs/libs/adapter-node/utils.d.ts +17 -0
- package/dist/cjs/libs/adapter-node/utils.js +221 -0
- package/dist/cjs/libs/request.d.ts +1 -0
- package/dist/cjs/libs/request.js +3 -2
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +36 -7
- package/dist/esm/libs/adapter-node/constants.d.ts +33 -0
- package/dist/esm/libs/adapter-node/constants.js +35 -0
- package/dist/esm/libs/adapter-node/context.d.ts +13 -0
- package/dist/esm/libs/adapter-node/context.js +156 -0
- package/dist/esm/libs/adapter-node/index.d.ts +23 -0
- package/dist/esm/libs/adapter-node/index.js +118 -0
- package/dist/esm/libs/adapter-node/metadata.d.ts +14 -0
- package/dist/esm/libs/adapter-node/metadata.js +123 -0
- package/dist/esm/libs/adapter-node/request.d.ts +38 -0
- package/dist/esm/libs/adapter-node/request.js +429 -0
- package/dist/esm/libs/adapter-node/tool.d.ts +2 -0
- package/dist/esm/libs/adapter-node/tool.js +223 -0
- package/dist/esm/libs/adapter-node/types.d.ts +78 -0
- package/dist/esm/libs/adapter-node/types.js +2 -0
- package/dist/esm/libs/adapter-node/utils.d.ts +17 -0
- package/dist/esm/libs/adapter-node/utils.js +205 -0
- package/dist/esm/libs/request.d.ts +1 -0
- package/dist/esm/libs/request.js +3 -2
- package/dist/miniprogram/index.js +1 -1
- package/package.json +20 -4
- package/src/index.ts +63 -7
- package/src/libs/adapter-node/constants.ts +42 -0
- package/src/libs/adapter-node/context.ts +238 -0
- package/src/libs/adapter-node/index.ts +166 -0
- package/src/libs/adapter-node/metadata.ts +69 -0
- package/src/libs/adapter-node/request.ts +480 -0
- package/src/libs/adapter-node/tool.ts +223 -0
- package/src/libs/adapter-node/types.ts +116 -0
- package/src/libs/adapter-node/utils.ts +182 -0
- package/src/libs/request.ts +4 -2
- package/webpack/web.prod.js +13 -13
- package/webpack/webpack.miniprogram.js +9 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** 自定义登录 Ticket 创建选项 */
|
|
2
|
+
export interface ICreateTicketOpts {
|
|
3
|
+
/** 刷新间隔(毫秒),默认 3600000(1小时) */
|
|
4
|
+
refresh?: number
|
|
5
|
+
/** 过期时间戳(毫秒),默认 7 天后 */
|
|
6
|
+
expire?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 用户信息查询结果 */
|
|
10
|
+
export interface IGetUserInfoResult {
|
|
11
|
+
openId: string
|
|
12
|
+
appId: string
|
|
13
|
+
uid: string
|
|
14
|
+
customUserId: string
|
|
15
|
+
isAnonymous: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 云函数入口 context 参数结构 */
|
|
19
|
+
export interface IContextParam {
|
|
20
|
+
memory_limit_in_mb: number
|
|
21
|
+
time_limit_in_ms: number
|
|
22
|
+
request_id: string
|
|
23
|
+
/** 老架构环境变量字符串,分号分隔的 key=value */
|
|
24
|
+
environ?: string
|
|
25
|
+
/** 新架构环境变量 JSON 字符串 */
|
|
26
|
+
environment?: string
|
|
27
|
+
function_version: string
|
|
28
|
+
function_name: string
|
|
29
|
+
namespace: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 用户信息查询请求参数 */
|
|
33
|
+
export interface IUserInfoQuery {
|
|
34
|
+
platform?: string
|
|
35
|
+
platformId?: string
|
|
36
|
+
uid?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 云函数运行时完整环境变量类型汇总
|
|
41
|
+
* 包含 SCF、TCB、微信相关的所有环境变量
|
|
42
|
+
*/
|
|
43
|
+
export interface ICompleteCloudbaseContext {
|
|
44
|
+
/** SCF 日志配置 */
|
|
45
|
+
_SCF_TCB_LOG?: string
|
|
46
|
+
/** 登录类型 */
|
|
47
|
+
LOGINTYPE?: string
|
|
48
|
+
QQ_APPID?: string
|
|
49
|
+
QQ_OPENID?: string
|
|
50
|
+
/** SCF 命名空间 */
|
|
51
|
+
SCF_NAMESPACE: string
|
|
52
|
+
/** TCB 上下文配置 */
|
|
53
|
+
TCB_CONTEXT_CNFG?: string
|
|
54
|
+
/** TCB 上下文 key 列表,逗号分隔 */
|
|
55
|
+
TCB_CONTEXT_KEYS: string[]
|
|
56
|
+
TCB_CUSTOM_USER_ID?: string
|
|
57
|
+
/** 云开发环境 ID */
|
|
58
|
+
TCB_ENV: string
|
|
59
|
+
TCB_HTTP_CONTEXT?: string
|
|
60
|
+
TCB_ISANONYMOUS_USER?: string
|
|
61
|
+
TCB_ROUTE_KEY?: string
|
|
62
|
+
/** 请求序列 ID */
|
|
63
|
+
TCB_SEQID: string
|
|
64
|
+
TCB_SESSIONTOKEN?: string
|
|
65
|
+
/** 客户端来源 IP */
|
|
66
|
+
TCB_SOURCE_IP?: string
|
|
67
|
+
TCB_SOURCE?: string
|
|
68
|
+
TCB_TRACELOG?: string
|
|
69
|
+
/** 用户唯一标识 */
|
|
70
|
+
TCB_UUID?: string
|
|
71
|
+
/** 腾讯云运行环境标识 */
|
|
72
|
+
TENCENTCLOUD_RUNENV: string
|
|
73
|
+
TENCENTCLOUD_SECRETID: string
|
|
74
|
+
TENCENTCLOUD_SECRETKEY: string
|
|
75
|
+
TENCENTCLOUD_SESSIONTOKEN: string
|
|
76
|
+
/** 触发来源 */
|
|
77
|
+
TRIGGER_SRC: string
|
|
78
|
+
WX_API_TOKEN?: string
|
|
79
|
+
WX_APPID?: string
|
|
80
|
+
WX_CLIENTIP?: string
|
|
81
|
+
WX_CLIENTIPV6?: string
|
|
82
|
+
WX_CLOUDBASE_ACCESSTOKEN?: string
|
|
83
|
+
/** 微信上下文 key 列表,逗号分隔 */
|
|
84
|
+
WX_CONTEXT_KEYS: string[]
|
|
85
|
+
WX_OPENID?: string
|
|
86
|
+
WX_TRIGGER_API_TOKEN_V0?: string
|
|
87
|
+
WX_UNIONID?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** getEndUserInfo 返回结果 */
|
|
91
|
+
export interface IGetEndUserInfoResult {
|
|
92
|
+
userInfo?: {
|
|
93
|
+
openId: string
|
|
94
|
+
appId: string
|
|
95
|
+
uid: string
|
|
96
|
+
customUserId: string
|
|
97
|
+
isAnonymous: boolean
|
|
98
|
+
}
|
|
99
|
+
requestId?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 自定义请求选项 */
|
|
103
|
+
export interface ICustomReqOpts {
|
|
104
|
+
/** 请求超时时间(毫秒) */
|
|
105
|
+
timeout?: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 模板消息推送请求参数 */
|
|
109
|
+
export interface ITemplateNotifyReq {
|
|
110
|
+
/** 通知策略 ID */
|
|
111
|
+
notifyId: string
|
|
112
|
+
/** 通知模板变量键值对 */
|
|
113
|
+
data?: Record<string, unknown>
|
|
114
|
+
/** 点击消息打开的页面地址 */
|
|
115
|
+
url?: string
|
|
116
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { utils } from '@cloudbase/utilities'
|
|
2
|
+
import { ReadableStream } from 'web-streams-polyfill'
|
|
3
|
+
import { ERROR, checkCustomUserIdRegex } from './constants'
|
|
4
|
+
import { lookupAppId } from './metadata'
|
|
5
|
+
|
|
6
|
+
/** 从 @cloudbase/utilities 中提取的表单和查询字符串工具 */
|
|
7
|
+
export const { isFormData, toQueryString } = utils as {
|
|
8
|
+
isFormData: (obj: unknown) => boolean
|
|
9
|
+
toQueryString: (data: Record<string, any>) => string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 跨端安全获取环境变量
|
|
14
|
+
* Web 端、小游戏端等没有 process 全局对象,直接访问 process.env 会报错
|
|
15
|
+
*/
|
|
16
|
+
export function getEnv(): Record<string, string | undefined>
|
|
17
|
+
export function getEnv(key: string): string | undefined
|
|
18
|
+
export function getEnv(key?: string): string | undefined | Record<string, string | undefined> {
|
|
19
|
+
if (typeof process === 'undefined' || !process.env) {
|
|
20
|
+
return key !== undefined ? undefined : {}
|
|
21
|
+
}
|
|
22
|
+
return key !== undefined ? process.env[key] : process.env
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 判断当前是否在 SCF(云函数)运行环境中 */
|
|
26
|
+
export const checkIsInScf = () => getEnv('TENCENTCLOUD_RUNENV') === 'SCF'
|
|
27
|
+
|
|
28
|
+
export function checkIsInCBR() {
|
|
29
|
+
// CBR = CLOUDBASE_RUN
|
|
30
|
+
return !!getEnv('CBR_ENV_ID')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const kSumeruEnvSet = new Set(['formal', 'pre', 'test'])
|
|
34
|
+
export function checkIsInSumeru() {
|
|
35
|
+
// SUMERU_ENV=formal | test | pre
|
|
36
|
+
return kSumeruEnvSet.has(getEnv('SUMERU_ENV') || '')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isNonEmptyString(str: string) {
|
|
40
|
+
return typeof str === 'string' && str !== ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function checkIsInTencentCloud(): Promise<boolean> {
|
|
44
|
+
if (getEnv('TENCENTCLOUD') === 'true') {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
return isNonEmptyString(await lookupAppId())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const getCurrRunEnvTag = async (): Promise<string> => {
|
|
51
|
+
if (checkIsInScf()) {
|
|
52
|
+
return 'scf'
|
|
53
|
+
} if (checkIsInCBR()) {
|
|
54
|
+
return 'cbr'
|
|
55
|
+
} if (checkIsInSumeru()) {
|
|
56
|
+
return 'sumeru'
|
|
57
|
+
} if (await checkIsInTencentCloud()) {
|
|
58
|
+
return 'tencentcloud'
|
|
59
|
+
}
|
|
60
|
+
return 'unknown'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 校验自定义用户 ID 格式
|
|
65
|
+
* @throws 格式不合法时抛出 INVALID_PARAM 错误
|
|
66
|
+
*/
|
|
67
|
+
export const validateUid = (uid: string): void => {
|
|
68
|
+
if (typeof uid !== 'string') {
|
|
69
|
+
throw { ...ERROR.INVALID_PARAM, message: 'uid must be a string' }
|
|
70
|
+
}
|
|
71
|
+
if (!checkCustomUserIdRegex.test(uid)) {
|
|
72
|
+
throw { ...ERROR.INVALID_PARAM, message: `Invalid uid: "${uid}"` }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 安全的 JSON 解析,解析失败时返回原始文本而非抛异常
|
|
78
|
+
* @param text - 待解析的字符串
|
|
79
|
+
* @returns 解析后的对象,空字符串返回 null,解析失败返回原始文本
|
|
80
|
+
*/
|
|
81
|
+
export function safeParseJson(text: string): any {
|
|
82
|
+
if (!text || !text.trim()) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(text)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn('catch an error', { e, text })
|
|
89
|
+
return text
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 将对象的所有值转为字符串类型
|
|
95
|
+
* 用于将 headers 等对象统一为 Record<string, string>
|
|
96
|
+
*/
|
|
97
|
+
export function obj2StrRecord(obj: object): Record<string, string> {
|
|
98
|
+
return Object.entries(obj).reduce<Record<string, string>>((acc, cur: [string, unknown]) => {
|
|
99
|
+
const [key, value] = cur
|
|
100
|
+
acc[key] = String(value)
|
|
101
|
+
return acc
|
|
102
|
+
}, {})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 将 HeadersInit 转为可索引的普通对象
|
|
107
|
+
* 兼容 Headers 实例和普通对象两种形式
|
|
108
|
+
*/
|
|
109
|
+
export function headersInit2Indexable(h: HeadersInit) {
|
|
110
|
+
if (isHeaders(h)) {
|
|
111
|
+
const ret: Record<string, string> = {}
|
|
112
|
+
h.forEach((val, key) => {
|
|
113
|
+
ret[key] = val
|
|
114
|
+
})
|
|
115
|
+
return ret
|
|
116
|
+
}
|
|
117
|
+
return h
|
|
118
|
+
|
|
119
|
+
function isHeaders(h: HeadersInit): h is Headers {
|
|
120
|
+
try {
|
|
121
|
+
// Node 低版本可能没有全局 Headers
|
|
122
|
+
return h instanceof Headers
|
|
123
|
+
} catch (_) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 手动解析查询字符串,避免 URLSearchParams 的兼容性问题
|
|
131
|
+
* 支持重复 key 自动转为数组
|
|
132
|
+
* @param search - 查询字符串,可带前导 ? 或 #
|
|
133
|
+
*/
|
|
134
|
+
export function parseQueryString(search: string): Record<string, string | string[]> {
|
|
135
|
+
const params: any = {}
|
|
136
|
+
const cleanSearch = search.replace(/^[?#]/, '')
|
|
137
|
+
|
|
138
|
+
if (!cleanSearch) {
|
|
139
|
+
return params
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pairs = cleanSearch.split('&')
|
|
143
|
+
|
|
144
|
+
pairs.forEach((item) => {
|
|
145
|
+
let [key, value] = item.split('=')
|
|
146
|
+
key = decodeURIComponent(key)
|
|
147
|
+
value = decodeURIComponent(value || '')
|
|
148
|
+
if (key) {
|
|
149
|
+
if (params[key]) {
|
|
150
|
+
// 已存在同名 key,转为数组
|
|
151
|
+
if (Array.isArray(params[key])) {
|
|
152
|
+
params[key].push(value)
|
|
153
|
+
} else {
|
|
154
|
+
params[key] = [params[key], value]
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
params[key] = value
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
return params
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 将 Node ReadableStream 转换为 Web ReadableStream (polyfill)
|
|
166
|
+
* 用于 fetch 流式响应在 Node 环境下的适配
|
|
167
|
+
*/
|
|
168
|
+
export function createWebStreamFromNodeReadableStream(stream: NodeJS.ReadableStream): ReadableStream {
|
|
169
|
+
const asyncIterator = stream[Symbol.asyncIterator]()
|
|
170
|
+
|
|
171
|
+
return new ReadableStream({
|
|
172
|
+
async pull(controller) {
|
|
173
|
+
const next = await asyncIterator.next()
|
|
174
|
+
|
|
175
|
+
if (next.done) {
|
|
176
|
+
controller.close()
|
|
177
|
+
} else {
|
|
178
|
+
controller.enqueue(next.value)
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
}
|
package/src/libs/request.ts
CHANGED
|
@@ -113,10 +113,11 @@ export class CloudbaseRequest implements ICloudbaseRequest {
|
|
|
113
113
|
*/
|
|
114
114
|
constructor(config: ICloudbaseRequestConfig & { throw?: boolean }) {
|
|
115
115
|
this.config = config
|
|
116
|
-
const reqConfig: IRequestConfig = {
|
|
116
|
+
const reqConfig: IRequestConfig & { auth: ICloudbaseRequestConfig['auth'] } = {
|
|
117
117
|
timeout: this.config.timeout,
|
|
118
118
|
timeoutMsg: `[@cloudbase/js-sdk] 请求在${this.config.timeout / 1000}s内未完成,已中断`,
|
|
119
119
|
restrictedMethods: ['post', 'put'],
|
|
120
|
+
auth: config.auth,
|
|
120
121
|
}
|
|
121
122
|
this.reqClass = new Platform.adapter.reqClass(reqConfig)
|
|
122
123
|
this.throwWhenRequestFail = config.throw || false
|
|
@@ -201,6 +202,7 @@ export class CloudbaseRequest implements ICloudbaseRequest {
|
|
|
201
202
|
search?: string
|
|
202
203
|
defaultQuery?: KV<any>
|
|
203
204
|
headers?: KV<string>
|
|
205
|
+
endPointMode?: 'CLOUD_API' | 'GATEWAY'
|
|
204
206
|
},
|
|
205
207
|
customReqOpts?: ICustomReqOpts,
|
|
206
208
|
): Promise<ResponseObject> {
|
|
@@ -284,7 +286,7 @@ export class CloudbaseRequest implements ICloudbaseRequest {
|
|
|
284
286
|
...formatQuery,
|
|
285
287
|
})
|
|
286
288
|
|
|
287
|
-
const endPointMode = this.config.endPointMode || 'CLOUD_API'
|
|
289
|
+
const endPointMode = options?.endPointMode || this.config.endPointMode || 'CLOUD_API'
|
|
288
290
|
|
|
289
291
|
const url = getEndPointInfo(this.config.env, endPointMode)
|
|
290
292
|
let BASE_URL = url.baseUrl
|
package/webpack/web.prod.js
CHANGED
|
@@ -5,7 +5,7 @@ const TerserPlugin = require('terser-webpack-plugin')
|
|
|
5
5
|
module.exports = function (options) {
|
|
6
6
|
const { context, entry, output, mode, watch, externals, definePlugin = {}, optimization = {} } = options
|
|
7
7
|
const isDevelopment = mode !== 'production'
|
|
8
|
-
|
|
8
|
+
const plugins = [
|
|
9
9
|
new webpack.DefinePlugin(definePlugin),
|
|
10
10
|
// new BundleAnalyzerPlugin()
|
|
11
11
|
].filter((item) => !!item)
|
|
@@ -82,19 +82,19 @@ module.exports = function (options) {
|
|
|
82
82
|
...optimization,
|
|
83
83
|
...(isDevelopment
|
|
84
84
|
? {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
minimize: false,
|
|
86
|
+
removeAvailableModules: false,
|
|
87
|
+
concatenateModules: true,
|
|
88
|
+
}
|
|
89
89
|
: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
minimizer: [
|
|
91
|
+
new TerserPlugin({
|
|
92
|
+
test: /\.js(\?.*)?$/i,
|
|
93
|
+
cache: false,
|
|
94
|
+
parallel: true,
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
98
98
|
},
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
+
const { ignoreNodeDepsPlugins } = require('../../../webpack.ignore-node-deps')
|
|
2
3
|
|
|
3
4
|
const params = {
|
|
4
5
|
context: path.resolve(__dirname, '../'),
|
|
@@ -8,17 +9,22 @@ const params = {
|
|
|
8
9
|
output: {
|
|
9
10
|
path: path.resolve(__dirname, '../dist/miniprogram'),
|
|
10
11
|
filename: 'index.js',
|
|
11
|
-
library:
|
|
12
|
+
library: 'tcboauth',
|
|
12
13
|
libraryTarget: 'umd',
|
|
13
14
|
umdNamedDefine: true,
|
|
14
15
|
globalObject: 'typeof window !== "undefined"?window:this',
|
|
15
16
|
},
|
|
16
17
|
externals: {},
|
|
17
18
|
definePlugin: {
|
|
18
|
-
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
19
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
19
20
|
'process.env.IS_MP_BUILD': 'true',
|
|
21
|
+
'process.env.IS_BROWSER_BUILD': 'true',
|
|
20
22
|
'globalThis.IS_MP_BUILD': true,
|
|
21
23
|
},
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
const config = require('./web.prod.js')(params)
|
|
27
|
+
|
|
28
|
+
config.plugins.push(...ignoreNodeDepsPlugins)
|
|
29
|
+
|
|
30
|
+
module.exports = config
|