wechat-ts 0.0.1

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.
Files changed (40) hide show
  1. package/README.md +3 -0
  2. package/dist/index.cjs +328 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +239 -0
  5. package/dist/index.d.ts +239 -0
  6. package/dist/index.global.js +321 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +283 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +41 -0
  11. package/src/constants/index.ts +2 -0
  12. package/src/constants/wechat.ts +4 -0
  13. package/src/index.ts +5 -0
  14. package/src/jssdk/getJssdkConfig.ts +122 -0
  15. package/src/jssdk/index.ts +1 -0
  16. package/src/oauth2/authorize.ts +95 -0
  17. package/src/oauth2/index.ts +1 -0
  18. package/src/server/cgi-bin/getAccessToken.ts +28 -0
  19. package/src/server/cgi-bin/getStableAccessToken.ts +24 -0
  20. package/src/server/cgi-bin/index.ts +3 -0
  21. package/src/server/cgi-bin/message/index.ts +1 -0
  22. package/src/server/cgi-bin/message/template/index.ts +1 -0
  23. package/src/server/cgi-bin/message/template/sendTemplateMessage.ts +41 -0
  24. package/src/server/cgi-bin/ticket/getTicket.ts +28 -0
  25. package/src/server/cgi-bin/ticket/index.ts +1 -0
  26. package/src/server/index.ts +2 -0
  27. package/src/server/types/response.ts +4 -0
  28. package/src/server/utils/request.ts +25 -0
  29. package/src/server/utils/token.ts +21 -0
  30. package/src/sns/getUserInfo.ts +49 -0
  31. package/src/sns/index.ts +3 -0
  32. package/src/sns/snsAccessToken.ts +47 -0
  33. package/src/sns/snsUserInfo.ts +49 -0
  34. package/src/types/wechat-api.d.ts +164 -0
  35. package/src/utils/config.ts +35 -0
  36. package/src/utils/promise.ts +14 -0
  37. package/src/utils/request.ts +20 -0
  38. package/src/utils/string.ts +33 -0
  39. package/tsconfig.json +15 -0
  40. package/tsup.config.ts +11 -0
@@ -0,0 +1,3 @@
1
+ export * from './getAccessToken'
2
+ export * from './getStableAccessToken'
3
+ export * from './message'
@@ -0,0 +1 @@
1
+ export * from './template'
@@ -0,0 +1 @@
1
+ export * from './sendTemplateMessage'
@@ -0,0 +1,41 @@
1
+ import { WechatResponse } from '../../../types/response'
2
+ import { requestPost } from '../../../utils/request'
3
+
4
+ /**
5
+ * 发送模板消息参数
6
+ * @interface sendTemplateMessageDTO
7
+ */
8
+ export interface SendTemplateMessageDTO {
9
+ /** 接收者openid */
10
+ touser: string
11
+ /** 模板ID */
12
+ template_id: string
13
+ /** 跳转URL */
14
+ url?: string
15
+ /** 小程序信息 */
16
+ miniprogram?: {
17
+ /** 小程序appid */
18
+ appid?: string
19
+ /** 小程序页面路径 */
20
+ pagepath?: string
21
+ }
22
+ /** 模板数据 */
23
+ data: Record<string, { value: string; color?: string }>
24
+ /** 防重入id。对于同一个openid + client_msg_id, 只发送一条消息,10分钟有效,超过10分钟不保证效果。若无防重入需求,可不填 */
25
+ client_msg_id?: string
26
+ }
27
+
28
+ export interface SendTemplateMessageVO extends WechatResponse {
29
+ msgid: number
30
+ }
31
+
32
+ /**
33
+ * 发送模板消息
34
+ * @param params
35
+ * @returns
36
+ *
37
+ * @see https://developers.weixin.qq.com/doc/service/api/notify/template/api_sendtemplatemessage.html
38
+ */
39
+ export function sendTemplateMessage(params: SendTemplateMessageDTO) {
40
+ return requestPost<SendTemplateMessageVO>('/cgi-bin/message/template/send', params)
41
+ }
@@ -0,0 +1,28 @@
1
+ import { WechatResponse } from "../../types/response"
2
+ import { requestGet } from "../../utils/request"
3
+
4
+ export interface GetTicketDTO {
5
+ /** ticket 类型,jsapi 为 js-sdk 凭证;wx_card 为微信卡券凭证 */
6
+ type: 'jsapi' | 'wx_card'
7
+ }
8
+
9
+ export interface GetTicketVO extends WechatResponse {
10
+ /** 临时票据 */
11
+ ticket: string
12
+ /**
13
+ * 有效期(秒)
14
+ * @default 7200
15
+ */
16
+ expires_in: number
17
+ }
18
+
19
+ /**
20
+ * 获取sdk临时票据
21
+ * @param data GetTicketDTO
22
+ * @returns GetTicketVO
23
+ *
24
+ * @see https://developers.weixin.qq.com/doc/service/api/webdev/jssdk/api_getticket.html
25
+ */
26
+ export function getTicket(data: GetTicketDTO) {
27
+ return requestGet<GetTicketVO>('/cgi-bin/ticket/getticket', data)
28
+ }
@@ -0,0 +1 @@
1
+ export * from './getTicket'
@@ -0,0 +1,2 @@
1
+ export * from './cgi-bin'
2
+ export * from './cgi-bin/message/template'
@@ -0,0 +1,4 @@
1
+ export interface WechatResponse {
2
+ errcode: number
3
+ errmsg: string
4
+ }
@@ -0,0 +1,25 @@
1
+ import { requestGetBase, requestPostBase } from '../../utils/request'
2
+ import { getToken } from './token'
3
+
4
+ async function urlAddToken(url: string) {
5
+ if (url.includes('?')) {
6
+ url += '&'
7
+ }
8
+ url += `access_token=${await getToken()}`
9
+ return url
10
+ }
11
+
12
+ export async function requestGet<T>(
13
+ url: string,
14
+ params: Record<string, any>,
15
+ options?: RequestInit
16
+ ) {
17
+ return requestGetBase<T>(await urlAddToken(url), params, options)
18
+ }
19
+ export async function requestPost<T>(
20
+ url: string,
21
+ data: Record<string, any>,
22
+ options?: RequestInit
23
+ ) {
24
+ return requestPostBase<T>(await urlAddToken(url), data, options)
25
+ }
@@ -0,0 +1,21 @@
1
+ import { getStableAccessToken } from '../cgi-bin/getStableAccessToken'
2
+ import { getConfig } from '../../utils/config'
3
+
4
+ let token = '',
5
+ expires = 0
6
+
7
+ export async function getToken() {
8
+ const { appId, appSecret } = getConfig()
9
+ const now = Date.now()
10
+ if (expires < now) {
11
+ const resToken = await getStableAccessToken({
12
+ appid: appId,
13
+ secret: appSecret,
14
+ grant_type: 'client_credential'
15
+ })
16
+ token = resToken.access_token
17
+ // 设置过期时间,留出5%的缓冲时间
18
+ expires = now + resToken.expires_in * 950
19
+ }
20
+ return token
21
+ }
@@ -0,0 +1,49 @@
1
+ import { snsAccessToken } from './snsAccessToken'
2
+ import { snsUserInfo } from './snsUserInfo'
3
+
4
+ /**
5
+ * 获取微信用户信息
6
+ * @param {WeixinGetUserInfoDTO} params - 获取微信用户信息参数
7
+ * @returns {Promise<SnsUserInfo>} 用户信息
8
+ */
9
+ export async function getSnsUserInfo(code: string) {
10
+ // 通过code换取网页授权access_token
11
+ // const { code } = params
12
+ // const accessToken: WeixinAccessToken = await OpenMpSnsAccessTokenApi.getSnsAccessToken(
13
+ // code,
14
+ // appid
15
+ // )
16
+ // console.log('accessToken:', accessToken)
17
+
18
+ // // 拉取用户信息(需scope为 snsapi_userinfo)
19
+ // const { access_token, openid } = accessToken
20
+ // const userInfo: TableUser = await OpenMpSnsAccessTokenApi.getUserInfo(
21
+ // access_token,
22
+ // openid,
23
+ // Lang.ZH_CN
24
+ // )
25
+
26
+ // if (userInfo.openid) {
27
+ // // 3. 使用user.ts中的函数保存用户信息
28
+ // saveUser(userInfo)
29
+ // return userInfo
30
+ // } else {
31
+ // throw new Error('获取用户信息失败')
32
+ // }
33
+ // 1. 获取access_token和openid
34
+ const tokenData = await snsAccessToken(code)
35
+ const { access_token, openid } = tokenData
36
+
37
+ if (!openid) {
38
+ throw new Error('获取openid失败')
39
+ }
40
+
41
+ // 2. 获取用户信息
42
+ const userInfo = await snsUserInfo({ access_token, openid })
43
+ if (userInfo.openid) {
44
+ // 3. 使用user.ts中的函数保存用户信息
45
+ return userInfo
46
+ } else {
47
+ throw new Error('获取用户信息失败')
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ export * from './snsAccessToken'
2
+ export * from './snsUserInfo'
3
+ export * from './getUserInfo'
@@ -0,0 +1,47 @@
1
+ import { getConfig } from '../utils/config'
2
+ import { requestGetBase } from '../utils/request'
3
+
4
+ /**
5
+ * 获取微信用户信息参数
6
+ * @interface WeixinGetUserInfoDTO
7
+ */
8
+ export interface WeixinGetUserInfoDTO {
9
+ /** 授权码 */
10
+ code: string
11
+ /** 重定向后会带上 state 参数,开发者可以填写任意参数值,最多 128 字节 */
12
+ state?: string
13
+ }
14
+
15
+ /**
16
+ * 微信访问令牌接口
17
+ */
18
+ export interface SnsAccessTokenVO {
19
+ /** 访问令牌 */
20
+ access_token: string
21
+ /** 过期时间(秒) */
22
+ expires_in: number
23
+ /** 刷新令牌 */
24
+ refresh_token: string
25
+ /** 用户openid */
26
+ openid: string
27
+ /** 授权作用域 */
28
+ scope: string
29
+ }
30
+
31
+ /**
32
+ * 获取微信访问令牌
33
+ * @param code 授权码
34
+ * @returns 微信访问令牌信息
35
+ *
36
+ * @see https://developers.weixin.qq.com/doc/service/api/webdev/access/api_snsaccesstoken.html
37
+ */
38
+ export async function snsAccessToken(code: string) {
39
+ const { appId, appSecret } = getConfig()
40
+ // 通过code换取网页授权access_token
41
+ return requestGetBase<SnsAccessTokenVO>('/sns/oauth2/access_token', {
42
+ appid: appId,
43
+ secret: appSecret,
44
+ code,
45
+ grant_type: 'authorization_code'
46
+ })
47
+ }
@@ -0,0 +1,49 @@
1
+ import { requestGetBase } from '../utils/request'
2
+ import { SnsAccessTokenVO } from './snsAccessToken'
3
+
4
+ export interface SnsUserInfoDTO extends Pick<SnsAccessTokenVO, 'access_token' | 'openid'> {
5
+ /**
6
+ * 语言
7
+ * @default zh_CN
8
+ */
9
+ lang?: 'zh_CN' | 'zh_TW' | 'en'
10
+ }
11
+
12
+ export interface SnsUserInfoVO {
13
+ /** 用户的微信openid */
14
+ openid?: string
15
+ /** 用户昵称 */
16
+ nickname?: string
17
+ /** 性别 0-未知 1-男 2-女 */
18
+ sex?: 0 | 1 | 2
19
+ /** 用户语言 */
20
+ language?: string
21
+ /** 用户所在城市 */
22
+ city?: string
23
+ /** 用户所在省份 */
24
+ province?: string
25
+ /** 用户所在国家 */
26
+ country?: string
27
+ /** 用户头像URL */
28
+ headimgurl?: string
29
+ /** 用户特权信息 */
30
+ privilege?: any[]
31
+ /** 用户在开放平台的唯一标识符 */
32
+ unionid?: string
33
+ }
34
+ /**
35
+ * 获取授权用户信息
36
+ * @param data SnsUserInfoDTO
37
+ * @returns SnsUserInfoVO
38
+ *
39
+ * @see https://developers.weixin.qq.com/doc/service/api/webdev/access/api_snsuserinfo.html
40
+ */
41
+ export async function snsUserInfo(data: SnsUserInfoDTO) {
42
+ const { access_token, openid, lang = 'zh_CN' } = data
43
+ // 2. 获取用户信息
44
+ return requestGetBase<SnsUserInfoVO>(`/sns/userinfo`, {
45
+ access_token: access_token,
46
+ openid: openid,
47
+ lang: lang
48
+ })
49
+ }
@@ -0,0 +1,164 @@
1
+ declare module 'wechat-api' {
2
+ // export interface WeixinGetJssdkConfigDTO {
3
+ // /** 当前页面URL */
4
+ // url: string
5
+ // /** 需要使用的JS API列表 */
6
+ // jsApiList?: string[]
7
+ // /** 是否开启调试模式 */
8
+ // debug?: boolean
9
+ // /** 微信硬件开发用 */
10
+ // beta?: boolean
11
+ // }
12
+
13
+ // /**
14
+ // * 微信JS-SDK配置接口
15
+ // */
16
+ // export interface WeixinGetJssdkConfigVO {
17
+ // /** 开启调试模式,调用的所有API的返回值会在客户端alert出来,若要查看传入的参数,可以在PC端打开,参数信息会通过log打出,仅在PC端时才会打印 */
18
+ // debug: boolean
19
+ // /** 微信硬件开发用 */
20
+ // beta?: boolean
21
+ // /** 必填,公众号的唯一标识 */
22
+ // appId: string
23
+ // /** 必填,生成签名的时间戳 */
24
+ // timestamp: string
25
+ // /** 必填,生成签名的随机串 */
26
+ // nonceStr: string
27
+ // /** 必填,签名 */
28
+ // signature: string
29
+ // /** 必填,需要使用的JS接口列表 */
30
+ // jsApiList: string[]
31
+ // }
32
+
33
+ /** 微信API错误码 */
34
+ export enum WechatErrorCode {
35
+ /** -1 */
36
+ SYSTEM_ERROR = -1,
37
+ /** 40001 */
38
+ INVALID_CREDENTIAL = 40001,
39
+ /** 40003 */
40
+ INVALID_OPENID = 40003,
41
+ /** 40008 */
42
+ INVALID_MESSAGE_TYPE = 40008,
43
+ /** 40013 */
44
+ INVALID_APPID = 40013,
45
+ /** 40036 */
46
+ INVALID_TEMPLATE_ID_SIZE = 40036,
47
+ /** 40037 */
48
+ INVALID_TEMPLATE_ID = 40037,
49
+ /** 40039 */
50
+ INVALID_URL_SIZE = 40039,
51
+ /** 40249 */
52
+ FORBID_MARKETING_CONTENT = 40249,
53
+ /** 43116 */
54
+ TEMPLATE_RESTRICTED = 43116,
55
+ /** 47003 */
56
+ PARAM_FORMAT_ERROR = 47003
57
+ }
58
+
59
+ /** 微信API错误码映射 */
60
+ export type WechatErrorMessage = {
61
+ [WechatErrorCode.SYSTEM_ERROR]: string
62
+ [WechatErrorCode.INVALID_CREDENTIAL]: string
63
+ [WechatErrorCode.INVALID_OPENID]: string
64
+ [WechatErrorCode.INVALID_MESSAGE_TYPE]: string
65
+ [WechatErrorCode.INVALID_APPID]: string
66
+ [WechatErrorCode.INVALID_TEMPLATE_ID_SIZE]: string
67
+ [WechatErrorCode.INVALID_TEMPLATE_ID]: string
68
+ [WechatErrorCode.INVALID_URL_SIZE]: string
69
+ [WechatErrorCode.FORBID_MARKETING_CONTENT]: string
70
+ [WechatErrorCode.TEMPLATE_RESTRICTED]: string
71
+ [WechatErrorCode.PARAM_FORMAT_ERROR]: string
72
+ }
73
+
74
+ /** 微信API错误码描述映射 */
75
+ export const WechatErrorMessages: WechatErrorMessage = {
76
+ [WechatErrorCode.SYSTEM_ERROR]: '系统繁忙,此时请开发者稍候再试',
77
+ [WechatErrorCode.INVALID_CREDENTIAL]: 'access_token isinvalid or not latest',
78
+ [WechatErrorCode.INVALID_OPENID]: '不合法的 OpenID',
79
+ [WechatErrorCode.INVALID_MESSAGE_TYPE]: '不合法的消息类型',
80
+ [WechatErrorCode.INVALID_APPID]: '不合法的 AppID',
81
+ [WechatErrorCode.INVALID_TEMPLATE_ID_SIZE]: '不合法的 template_id 长度',
82
+ [WechatErrorCode.INVALID_TEMPLATE_ID]: '不合法的 template_id',
83
+ [WechatErrorCode.INVALID_URL_SIZE]: '不合法的 URL 长度',
84
+ [WechatErrorCode.FORBID_MARKETING_CONTENT]: '禁止发送营销内容',
85
+ [WechatErrorCode.TEMPLATE_RESTRICTED]: '模板被限制下发',
86
+ [WechatErrorCode.PARAM_FORMAT_ERROR]: '参数格式错误'
87
+ }
88
+ export interface WechatAPICallbackResult {
89
+ errcode: 0
90
+ errmsg: 'ok'
91
+ msgid: number
92
+ }
93
+ export interface WechatAPICallbackError {
94
+ errcode: number
95
+ errmsg: string
96
+ }
97
+ export type WechatAPICallback<T = WechatAPICallbackError> = (err: WechatAPICallbackError, result: T) => void
98
+
99
+ export default class WechatAPI {
100
+ /**
101
+ * 初始化
102
+ * @param appId appId
103
+ * @param appSecret 秘钥
104
+ */
105
+ constructor(appId: string, appSecret: string)
106
+ getAccessToken(callback: WechatAPICallback): void
107
+
108
+ getJsConfig(
109
+ params: WeixinGetJssdkConfigDTO,
110
+ callback: WechatAPICallback<WeixinGetJssdkConfigVO>
111
+ ): void
112
+
113
+ /**
114
+ * 发送模板消息
115
+ * 详细细节: https://developers.weixin.qq.com/doc/service/api/notify/template/api_sendtemplatemessage.html
116
+ * Examples:
117
+ * ```
118
+ * var templateId: '模板id';
119
+ * // URL置空,则在发送后,点击模板消息会进入一个空白页面(ios), 或无法点击(android)
120
+ * var url: 'http://weixin.qq.com/download';
121
+ * var data = {
122
+ * "first": {
123
+ * "value":"恭喜你购买成功!",
124
+ * "color":"#173177"
125
+ * },
126
+ * "keyword1":{
127
+ * "value":"巧克力",
128
+ * "color":"#173177"
129
+ * },
130
+ * "keyword2": {
131
+ * "value":"39.8元",
132
+ * "color":"#173177"
133
+ * },
134
+ * "keyword3": {
135
+ * "value":"2014年9月22日",
136
+ * "color":"#173177"
137
+ * },
138
+ * "remark":{
139
+ * "value":"欢迎再次购买!",
140
+ * "color":"#173177"
141
+ * }
142
+ * };
143
+ * api.sendTemplate('openid', templateId, dest, data, callback);
144
+ * ```
145
+ * Callback:
146
+ *
147
+ * - `err`, 调用失败时得到的异常
148
+ * - `result`, 调用正常时得到的对象
149
+ *
150
+ * @param {String} openid 用户的openid
151
+ * @param {String} templateId 模板ID
152
+ * @param {Object} dest 跳转目的地置空,则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android)
153
+ * @param {Object} data 渲染模板的数据
154
+ * @param {Function} callback 回调函数
155
+ */
156
+ sendTemplate(
157
+ openid: string,
158
+ templateId: string,
159
+ dest: string | { url: string; miniprogram: string },
160
+ data: Record<string, { value: string; color?: string }>,
161
+ callback: WechatAPICallback
162
+ )
163
+ }
164
+ }
@@ -0,0 +1,35 @@
1
+ // 微信配置
2
+ export let appId = ''
3
+ export let appSecret = ''
4
+
5
+ /** WechatAPI实例 */
6
+ // let api: WechatAPI
7
+ // export function createApi() {
8
+ // getConfig()
9
+ // if (!api) {
10
+ // api = new WechatAPI(appId, appSecret)
11
+ // }
12
+ // return api
13
+ // }
14
+
15
+ export interface WechatConfig {
16
+ /** 微信公众号appid */
17
+ appId: string
18
+ /** 微信公众号appsecret */
19
+ appSecret: string
20
+ }
21
+
22
+ export function setConfig(config: WechatConfig) {
23
+ appId = config.appId
24
+ appSecret = config.appSecret
25
+ }
26
+
27
+ export function getConfig(): WechatConfig {
28
+ if (!appId || !appSecret) {
29
+ throw new Error('请先调用setConfig配置appId/appSecret')
30
+ }
31
+ return {
32
+ appId,
33
+ appSecret
34
+ }
35
+ }
@@ -0,0 +1,14 @@
1
+ export function usePromiseWithResolvers<T>() {
2
+ let resolve: (value: T) => void = () => {},
3
+ reject: (reason?: any) => void= () => {}
4
+ const promise = new Promise<T>((_resolve, _reject) => {
5
+ resolve = _resolve
6
+ reject = _reject
7
+ })
8
+
9
+ return {
10
+ promise,
11
+ resolve,
12
+ reject
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ import { baseDemain } from '../constants'
2
+ export function requestGetBase<T>(
3
+ url: string,
4
+ params: Record<string, any>,
5
+ options: RequestInit = {}
6
+ ) {
7
+ const urlParams = new URLSearchParams(params)
8
+ return requestBase<T>(`${url}?${urlParams}`, { ...options, method: 'GET' })
9
+ }
10
+ export function requestPostBase<T>(
11
+ url: string,
12
+ data: Record<string, any>,
13
+ options: RequestInit = {}
14
+ ) {
15
+ return requestBase<T>(url, { ...options, method: 'POST', body: JSON.stringify(data) })
16
+ }
17
+ export async function requestBase<T>(url: string, options: RequestInit = {}) {
18
+ const res = await fetch(`${baseDemain}${url}`, options)
19
+ return (await res.json()) as T
20
+ }
@@ -0,0 +1,33 @@
1
+ /*!
2
+ * 生成随机字符串
3
+ */
4
+ export const createNonceStr = function () {
5
+ return Math.random().toString(36).substr(2, 15)
6
+ }
7
+
8
+ /*!
9
+ * 生成时间戳
10
+ */
11
+ export const createTimestamp = function () {
12
+ return Math.round(new Date().getTime() / 1000) + ''
13
+ }
14
+
15
+ /*!
16
+ * 排序查询字符串
17
+ */
18
+ export const raw = function (args: Record<string, string | number>) {
19
+ var keys = Object.keys(args)
20
+ keys = keys.sort()
21
+ var newArgs: typeof args = {}
22
+ keys.forEach(function (key) {
23
+ newArgs[key.toLowerCase()] = args[key]
24
+ })
25
+
26
+ var string = ''
27
+ var newKeys = Object.keys(newArgs)
28
+ for (var i = 0; i < newKeys.length; i++) {
29
+ var k = newKeys[i]
30
+ string += '&' + k + '=' + newArgs[k]
31
+ }
32
+ return string.substr(1)
33
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "sourceMap": true,
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["index.ts", "src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ outDir: 'dist',
6
+ format: ['cjs', 'esm', 'iife'],
7
+ globalName:'wechat',
8
+ dts: true,
9
+ sourcemap: true,
10
+ clean: true,
11
+ })