feat: Improve Push Notification (#7667)
* clean up * ev => data * refactor * clean up * add type * antenna * channel * fix * add Packed type * add PackedRef * fix lint * add emoji schema * add reversiGame * add reversiMatching * remove signin schema (use Signin entity) * add schemas refs, fix Packed type * wip PackedHoge => Packed<'Hoge'> * add Packed type * note-reaction * user * user-group * user-list * note * app, messaging-message * notification * drive-file * drive-folder * following * muting * blocking * hashtag * page * app (with modifying schema) * import user? * channel * antenna * clip * gallery-post * emoji * Packed * reversi-matching * update stream.ts * https://github.com/misskey-dev/misskey/pull/7769#issuecomment-917542339 * fix lint * clean up? * add app * fix * nanka iroiro * wip * wip * fix lint * fix loginId * fix * refactor * refactor * remove follow action * clean up * Revert "remove follow action" This reverts commit defbb416480905af2150d1c92f10d8e1d1288c0a. * Revert "clean up" This reverts commit f94919cb9cff41e274044fc69c56ad36a33974f2. * remove fetch specification * renoteの条件追加 * apiFetch => cli * bypass fetch? * fix * refactor: use path alias * temp: add submodule * remove submodule * enhane: unison-reloadに指定したパスに移動できるように * null * null * feat: ログインするアカウントのIDをクエリ文字列で指定する機能 * null * await? * rename * rename * Update read.ts * merge * get-note-summary * fix * swパッケージに * add missing packages * fix getNoteSummary * add webpack-cli * ✌️ * remove plugins * sw-inject分離したがテストしてない * fix notification.vue * remove a blank line * disconnect intersection observer * disconnect2 * fix notification.vue * remove a blank line * disconnect intersection observer * disconnect2 * fix * ✌️ * clean up config * typesを戻した * Update packages/client/src/components/notification.vue Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * disconnect * oops * Failed to load the script unexpectedly回避 sw.jsとlib.tsを分離してみた * truncate notification * Update packages/client/src/ui/_common_/common.vue Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * clean up * clean up * キャッシュ対策 * Truncate push notification message * クライアントがあったらストリームに接続しているということなので通知しない判定の位置を修正 * components/drive-file-thumbnail.vue * components/drive-select-dialog.vue * components/drive-window.vue * merge * fix * Service Workerのビルドにesbuildを使うようにする * return createEmptyNotification() * fix * i18n.ts * update * ✌️ * remove ts-loader * fix * fix * enhance: Service Workerを常に登録するように * pollEnded * URLをsw.jsに戻す * clean up Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									1c6d5ddf81
								
							
						
					
					
						commit
						766559c6e9
					
				
					 47 changed files with 1834 additions and 298 deletions
				
			
		|  | @ -2,7 +2,9 @@ | ||||||
| ## 12.x.x (unreleased) | ## 12.x.x (unreleased) | ||||||
| 
 | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
| -  | - API: notifications/readは配列でも受け付けるように | ||||||
|  | - /share のクエリでリプライやファイル等の情報を渡せるように | ||||||
|  | - ページロードエラーページにリロードボタンを追加 | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| -  | -  | ||||||
|  |  | ||||||
|  | @ -356,7 +356,7 @@ antennaExcludeKeywords: "除外キーワード" | ||||||
| antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" | antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" | ||||||
| notifyAntenna: "新しいノートを通知する" | notifyAntenna: "新しいノートを通知する" | ||||||
| withFileAntenna: "ファイルが添付されたノートのみ" | withFileAntenna: "ファイルが添付されたノートのみ" | ||||||
| enableServiceworker: "ServiceWorkerを有効にする" | enableServiceworker: "ブラウザへのプッシュ通知を有効にする" | ||||||
| antennaUsersDescription: "ユーザー名を改行で区切って指定します" | antennaUsersDescription: "ユーザー名を改行で区切って指定します" | ||||||
| caseSensitive: "大文字小文字を区別する" | caseSensitive: "大文字小文字を区別する" | ||||||
| withReplies: "返信を含む" | withReplies: "返信を含む" | ||||||
|  | @ -1668,8 +1668,9 @@ _notification: | ||||||
|   youWereFollowed: "フォローされました" |   youWereFollowed: "フォローされました" | ||||||
|   youReceivedFollowRequest: "フォローリクエストが来ました" |   youReceivedFollowRequest: "フォローリクエストが来ました" | ||||||
|   yourFollowRequestAccepted: "フォローリクエストが承認されました" |   yourFollowRequestAccepted: "フォローリクエストが承認されました" | ||||||
|   youWereInvitedToGroup: "グループに招待されました" |   youWereInvitedToGroup: "{userName}があなたをグループに招待しました" | ||||||
|   pollEnded: "アンケートの結果が出ました" |   pollEnded: "アンケートの結果が出ました" | ||||||
|  |   emptyPushNotificationMessage: "プッシュ通知の更新をしました" | ||||||
| 
 | 
 | ||||||
|   _types: |   _types: | ||||||
|     all: "すべて" |     all: "すべて" | ||||||
|  | @ -1686,6 +1687,11 @@ _notification: | ||||||
|     groupInvited: "グループに招待された" |     groupInvited: "グループに招待された" | ||||||
|     app: "連携アプリからの通知" |     app: "連携アプリからの通知" | ||||||
| 
 | 
 | ||||||
|  |   _actions: | ||||||
|  |     followBack: "フォローバック" | ||||||
|  |     reply: "返信" | ||||||
|  |     renote: "Renote" | ||||||
|  | 
 | ||||||
| _deck: | _deck: | ||||||
|   alwaysShowMainColumn: "常にメインカラムを表示" |   alwaysShowMainColumn: "常にメインカラムを表示" | ||||||
|   columnAlign: "カラムの寄せ" |   columnAlign: "カラムの寄せ" | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; | import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; | ||||||
| import { publishMessagingStream } from '@/services/stream.js'; | import { publishMessagingStream } from '@/services/stream.js'; | ||||||
| import { publishMessagingIndexStream } from '@/services/stream.js'; | import { publishMessagingIndexStream } from '@/services/stream.js'; | ||||||
|  | import { pushNotification } from '@/services/push-notification.js'; | ||||||
| import { User, IRemoteUser } from '@/models/entities/user.js'; | import { User, IRemoteUser } from '@/models/entities/user.js'; | ||||||
| import { MessagingMessage } from '@/models/entities/messaging-message.js'; | import { MessagingMessage } from '@/models/entities/messaging-message.js'; | ||||||
| import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; | import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; | ||||||
|  | @ -50,6 +51,21 @@ export async function readUserMessagingMessage( | ||||||
| 	if (!await Users.getHasUnreadMessagingMessage(userId)) { | 	if (!await Users.getHasUnreadMessagingMessage(userId)) { | ||||||
| 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||||
| 		publishMainStream(userId, 'readAllMessagingMessages'); | 		publishMainStream(userId, 'readAllMessagingMessages'); | ||||||
|  | 		pushNotification(userId, 'readAllMessagingMessages', undefined); | ||||||
|  | 	} else { | ||||||
|  | 		// そのユーザーとのメッセージで未読がなければイベント発行
 | ||||||
|  | 		const count = await MessagingMessages.count({ | ||||||
|  | 			where: { | ||||||
|  | 				userId: otherpartyId, | ||||||
|  | 				recipientId: userId, | ||||||
|  | 				isRead: false, | ||||||
|  | 			}, | ||||||
|  | 			take: 1 | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (!count) { | ||||||
|  | 			pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -104,6 +120,19 @@ export async function readGroupMessagingMessage( | ||||||
| 	if (!await Users.getHasUnreadMessagingMessage(userId)) { | 	if (!await Users.getHasUnreadMessagingMessage(userId)) { | ||||||
| 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||||
| 		publishMainStream(userId, 'readAllMessagingMessages'); | 		publishMainStream(userId, 'readAllMessagingMessages'); | ||||||
|  | 		pushNotification(userId, 'readAllMessagingMessages', undefined); | ||||||
|  | 	} else { | ||||||
|  | 		// そのグループにおいて未読がなければイベント発行
 | ||||||
|  | 		const unreadExist = await MessagingMessages.createQueryBuilder('message') | ||||||
|  | 			.where(`message.groupId = :groupId`, { groupId: groupId }) | ||||||
|  | 			.andWhere('message.userId != :userId', { userId: userId }) | ||||||
|  | 			.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) | ||||||
|  | 			.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
 | ||||||
|  | 			.getOne().then(x => x != null); | ||||||
|  | 
 | ||||||
|  | 		if (!unreadExist) { | ||||||
|  | 			pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { publishMainStream } from '@/services/stream.js'; | import { publishMainStream } from '@/services/stream.js'; | ||||||
|  | import { pushNotification } from '@/services/push-notification.js'; | ||||||
| import { User } from '@/models/entities/user.js'; | import { User } from '@/models/entities/user.js'; | ||||||
| import { Notification } from '@/models/entities/notification.js'; | import { Notification } from '@/models/entities/notification.js'; | ||||||
| import { Notifications, Users } from '@/models/index.js'; | import { Notifications, Users } from '@/models/index.js'; | ||||||
|  | @ -16,28 +17,29 @@ export async function readNotification( | ||||||
| 		isRead: true, | 		isRead: true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	post(userId); | 	if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId); | ||||||
|  | 	else return postReadNotifications(userId, notificationIds); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function readNotificationByQuery( | export async function readNotificationByQuery( | ||||||
| 	userId: User['id'], | 	userId: User['id'], | ||||||
| 	query: Record<string, any> | 	query: Record<string, any> | ||||||
| ) { | ) { | ||||||
| 	// Update documents
 | 	const notificationIds = await Notifications.find({ | ||||||
| 	await Notifications.update({ |  | ||||||
| 		...query, | 		...query, | ||||||
| 		notifieeId: userId, | 		notifieeId: userId, | ||||||
| 		isRead: false, | 		isRead: false, | ||||||
| 	}, { | 	}).then(notifications => notifications.map(notification => notification.id)); | ||||||
| 		isRead: true, |  | ||||||
| 	}); |  | ||||||
| 
 | 
 | ||||||
| 	post(userId); | 	return readNotification(userId, notificationIds); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function post(userId: User['id']) { | function postReadAllNotifications(userId: User['id']) { | ||||||
| 	if (!await Users.getHasUnreadNotification(userId)) { |  | ||||||
| 		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
 |  | ||||||
| 	publishMainStream(userId, 'readAllNotifications'); | 	publishMainStream(userId, 'readAllNotifications'); | ||||||
| 	} | 	return pushNotification(userId, 'readAllNotifications', undefined); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { | ||||||
|  | 	publishMainStream(userId, 'readNotifications', notificationIds); | ||||||
|  | 	return pushNotification(userId, 'readNotifications', { notificationIds }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { publishMainStream } from '@/services/stream.js'; | import { publishMainStream } from '@/services/stream.js'; | ||||||
|  | import { pushNotification } from '@/services/push-notification.js'; | ||||||
| import define from '../../define.js'; | import define from '../../define.js'; | ||||||
| import { Notifications } from '@/models/index.js'; | import { Notifications } from '@/models/index.js'; | ||||||
| 
 | 
 | ||||||
|  | @ -28,4 +29,5 @@ export default define(meta, paramDef, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	// 全ての通知を読みましたよというイベントを発行
 | 	// 全ての通知を読みましたよというイベントを発行
 | ||||||
| 	publishMainStream(user.id, 'readAllNotifications'); | 	publishMainStream(user.id, 'readAllNotifications'); | ||||||
|  | 	pushNotification(user.id, 'readAllNotifications', undefined); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| import { publishMainStream } from '@/services/stream.js'; |  | ||||||
| import define from '../../define.js'; | import define from '../../define.js'; | ||||||
| import { Notifications } from '@/models/index.js'; |  | ||||||
| import { readNotification } from '../../common/read-notification.js'; | import { readNotification } from '../../common/read-notification.js'; | ||||||
| import { ApiError } from '../../error.js'; |  | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '通知を既読にします。', | ||||||
|  | 		'en-US': 'Mark a notification as read.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	tags: ['notifications', 'account'], | 	tags: ['notifications', 'account'], | ||||||
| 
 | 
 | ||||||
| 	requireCredential: true, | 	requireCredential: true, | ||||||
|  | @ -21,23 +23,26 @@ export const meta = { | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| export const paramDef = { | export const paramDef = { | ||||||
|  | 	oneOf: [ | ||||||
|  | 		{ | ||||||
| 			type: 'object', | 			type: 'object', | ||||||
| 			properties: { | 			properties: { | ||||||
| 				notificationId: { type: 'string', format: 'misskey:id' }, | 				notificationId: { type: 'string', format: 'misskey:id' }, | ||||||
| 			}, | 			}, | ||||||
| 			required: ['notificationId'], | 			required: ['notificationId'], | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			type: 'object', | ||||||
|  | 			properties: { | ||||||
|  | 				notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } }, | ||||||
|  | 			}, | ||||||
|  | 			required: ['notificationIds'], | ||||||
|  | 		}, | ||||||
|  | 	], | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line import/no-default-export
 | // eslint-disable-next-line import/no-default-export
 | ||||||
| export default define(meta, paramDef, async (ps, user) => { | export default define(meta, paramDef, async (ps, user) => { | ||||||
| 	const notification = await Notifications.findOneBy({ | 	if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]); | ||||||
| 		notifieeId: user.id, | 	return readNotification(user.id, ps.notificationIds); | ||||||
| 		id: ps.notificationId, |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	if (notification == null) { |  | ||||||
| 		throw new ApiError(meta.errors.noSuchNotification); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	readNotification(user.id, [notification.id]); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ const _dirname = dirname(_filename); | ||||||
| const staticAssets = `${_dirname}/../../../assets/`; | const staticAssets = `${_dirname}/../../../assets/`; | ||||||
| const clientAssets = `${_dirname}/../../../../client/assets/`; | const clientAssets = `${_dirname}/../../../../client/assets/`; | ||||||
| const assets = `${_dirname}/../../../../../built/_client_dist_/`; | const assets = `${_dirname}/../../../../../built/_client_dist_/`; | ||||||
|  | const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; | ||||||
| 
 | 
 | ||||||
| // Init app
 | // Init app
 | ||||||
| const app = new Koa(); | const app = new Koa(); | ||||||
|  | @ -136,9 +137,10 @@ router.get('/twemoji/(.*)', async ctx => { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // ServiceWorker
 | // ServiceWorker
 | ||||||
| router.get('/sw.js', async ctx => { | router.get(`/sw.js`, async ctx => { | ||||||
| 	await send(ctx as any, `/sw.${config.version}.js`, { | 	await send(ctx as any, `/sw.js`, { | ||||||
| 		root: assets, | 		root: swAssets, | ||||||
|  | 		maxage: ms('10 minutes'), | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { publishMainStream } from '@/services/stream.js'; | import { publishMainStream } from '@/services/stream.js'; | ||||||
| import pushSw from './push-notification.js'; | import { pushNotification } from '@/services/push-notification.js'; | ||||||
| import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; | import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; | ||||||
| import { genId } from '@/misc/gen-id.js'; | import { genId } from '@/misc/gen-id.js'; | ||||||
| import { User } from '@/models/entities/user.js'; | import { User } from '@/models/entities/user.js'; | ||||||
|  | @ -52,8 +52,8 @@ export async function createNotification( | ||||||
| 		//#endregion
 | 		//#endregion
 | ||||||
| 
 | 
 | ||||||
| 		publishMainStream(notifieeId, 'unreadNotification', packed); | 		publishMainStream(notifieeId, 'unreadNotification', packed); | ||||||
|  | 		pushNotification(notifieeId, 'notification', packed); | ||||||
| 
 | 
 | ||||||
| 		pushSw(notifieeId, 'notification', packed); |  | ||||||
| 		if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); | 		if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); | ||||||
| 		if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); | 		if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); | ||||||
| 	}, 2000); | 	}, 2000); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/i | ||||||
| import { genId } from '@/misc/gen-id.js'; | import { genId } from '@/misc/gen-id.js'; | ||||||
| import { MessagingMessage } from '@/models/entities/messaging-message.js'; | import { MessagingMessage } from '@/models/entities/messaging-message.js'; | ||||||
| import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; | import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; | ||||||
| import pushNotification from '../push-notification.js'; | import { pushNotification } from '@/services/push-notification.js'; | ||||||
| import { Not } from 'typeorm'; | import { Not } from 'typeorm'; | ||||||
| import { Note } from '@/models/entities/note.js'; | import { Note } from '@/models/entities/note.js'; | ||||||
| import renderNote from '@/remote/activitypub/renderer/note.js'; | import renderNote from '@/remote/activitypub/renderer/note.js'; | ||||||
|  |  | ||||||
|  | @ -5,8 +5,15 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||||
| import { Packed } from '@/misc/schema.js'; | import { Packed } from '@/misc/schema.js'; | ||||||
| import { getNoteSummary } from '@/misc/get-note-summary.js'; | import { getNoteSummary } from '@/misc/get-note-summary.js'; | ||||||
| 
 | 
 | ||||||
| type notificationType = 'notification' | 'unreadMessagingMessage'; | // Defined also packages/sw/types.ts#L14-L21
 | ||||||
| type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>; | type pushNotificationsTypes = { | ||||||
|  | 	'notification': Packed<'Notification'>; | ||||||
|  | 	'unreadMessagingMessage': Packed<'MessagingMessage'>; | ||||||
|  | 	'readNotifications': { notificationIds: string[] }; | ||||||
|  | 	'readAllNotifications': undefined; | ||||||
|  | 	'readAllMessagingMessages': undefined; | ||||||
|  | 	'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| // プッシュメッセージサーバーには文字数制限があるため、内容を削減します
 | // プッシュメッセージサーバーには文字数制限があるため、内容を削減します
 | ||||||
| function truncateNotification(notification: Packed<'Notification'>): any { | function truncateNotification(notification: Packed<'Notification'>): any { | ||||||
|  | @ -17,20 +24,19 @@ function truncateNotification(notification: Packed<'Notification'>): any { | ||||||
| 				...notification.note, | 				...notification.note, | ||||||
| 				// textをgetNoteSummaryしたものに置き換える
 | 				// textをgetNoteSummaryしたものに置き換える
 | ||||||
| 				text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), | 				text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), | ||||||
| 				...{ | 
 | ||||||
| 				cw: undefined, | 				cw: undefined, | ||||||
| 				reply: undefined, | 				reply: undefined, | ||||||
| 				renote: undefined, | 				renote: undefined, | ||||||
| 				user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
 | 				user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
 | ||||||
| 			} | 			} | ||||||
| 			} |  | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return notification; | 	return notification; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default async function(userId: string, type: notificationType, body: notificationBody) { | export async function pushNotification<T extends keyof pushNotificationsTypes>(userId: string, type: T, body: pushNotificationsTypes[T]) { | ||||||
| 	const meta = await fetchMeta(); | 	const meta = await fetchMeta(); | ||||||
| 
 | 
 | ||||||
| 	if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; | 	if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; | ||||||
|  |  | ||||||
|  | @ -72,7 +72,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, ref, onMounted, onUnmounted } from 'vue'; | import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import { getNoteSummary } from '@/scripts/get-note-summary'; | import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||||
| import XReactionIcon from './reaction-icon.vue'; | import XReactionIcon from './reaction-icon.vue'; | ||||||
|  | @ -126,6 +126,10 @@ export default defineComponent({ | ||||||
| 				const connection = stream.useChannel('main'); | 				const connection = stream.useChannel('main'); | ||||||
| 				connection.on('readAllNotifications', () => readObserver.disconnect()); | 				connection.on('readAllNotifications', () => readObserver.disconnect()); | ||||||
| 
 | 
 | ||||||
|  | 				watch(props.notification.isRead, () => { | ||||||
|  | 					readObserver.disconnect(); | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
| 				onUnmounted(() => { | 				onUnmounted(() => { | ||||||
| 					readObserver.disconnect(); | 					readObserver.disconnect(); | ||||||
| 					connection.dispose(); | 					connection.dispose(); | ||||||
|  |  | ||||||
|  | @ -64,6 +64,31 @@ const onNotification = (notification) => { | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	const connection = stream.useChannel('main'); | 	const connection = stream.useChannel('main'); | ||||||
| 	connection.on('notification', onNotification); | 	connection.on('notification', onNotification); | ||||||
|  | 	connection.on('readAllNotifications', () => { | ||||||
|  | 		if (pagingComponent.value) { | ||||||
|  | 			for (const item of pagingComponent.value.queue) { | ||||||
|  | 				item.isRead = true; | ||||||
|  | 			} | ||||||
|  | 			for (const item of pagingComponent.value.items) { | ||||||
|  | 				item.isRead = true; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 	connection.on('readNotifications', notificationIds => { | ||||||
|  | 		if (pagingComponent.value) { | ||||||
|  | 			for (let i = 0; i < pagingComponent.value.queue.length; i++) { | ||||||
|  | 				if (notificationIds.includes(pagingComponent.value.queue[i].id)) { | ||||||
|  | 					pagingComponent.value.queue[i].isRead = true; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			for (let i = 0; i < (pagingComponent.value.items || []).length; i++) { | ||||||
|  | 				if (notificationIds.includes(pagingComponent.value.items[i].id)) { | ||||||
|  | 					pagingComponent.value.items[i].isRead = true; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	onUnmounted(() => { | 	onUnmounted(() => { | ||||||
| 		connection.dispose(); | 		connection.dispose(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | @ -270,6 +270,7 @@ onDeactivated(() => { | ||||||
| 
 | 
 | ||||||
| defineExpose({ | defineExpose({ | ||||||
| 	items, | 	items, | ||||||
|  | 	queue, | ||||||
| 	backed, | 	backed, | ||||||
| 	reload, | 	reload, | ||||||
| 	fetchMoreAhead, | 	fetchMoreAhead, | ||||||
|  |  | ||||||
|  | @ -146,7 +146,6 @@ if ($i && $i.token) { | ||||||
| 		try { | 		try { | ||||||
| 			document.body.innerHTML = '<div>Please wait...</div>'; | 			document.body.innerHTML = '<div>Please wait...</div>'; | ||||||
| 			await login(i); | 			await login(i); | ||||||
| 			location.reload(); |  | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
| 			// Render the error screen
 | 			// Render the error screen
 | ||||||
| 			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
 | 			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
 | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								packages/client/src/scripts/get-user-name.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/client/src/scripts/get-user-name.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export default function(user: { name?: string | null, username: string }): string { | ||||||
|  | 	return user.name || user.username; | ||||||
|  | } | ||||||
|  | @ -4,22 +4,22 @@ import { api } from '@/os'; | ||||||
| import { lang } from '@/config'; | import { lang } from '@/config'; | ||||||
| 
 | 
 | ||||||
| export async function initializeSw() { | export async function initializeSw() { | ||||||
| 	if (instance.swPublickey && | 	if (!('serviceWorker' in navigator)) return; | ||||||
| 		('serviceWorker' in navigator) && |  | ||||||
| 		('PushManager' in window) && |  | ||||||
| 		$i && $i.token) { |  | ||||||
| 		navigator.serviceWorker.register(`/sw.js`); |  | ||||||
| 
 | 
 | ||||||
|  | 	navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); | ||||||
| 	navigator.serviceWorker.ready.then(registration => { | 	navigator.serviceWorker.ready.then(registration => { | ||||||
| 		registration.active?.postMessage({ | 		registration.active?.postMessage({ | ||||||
| 			msg: 'initialize', | 			msg: 'initialize', | ||||||
| 			lang, | 			lang, | ||||||
| 		}); | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) { | ||||||
| 			// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
 | 			// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
 | ||||||
| 			registration.pushManager.subscribe({ | 			registration.pushManager.subscribe({ | ||||||
| 				userVisibleOnly: true, | 				userVisibleOnly: true, | ||||||
| 				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey) | 				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey) | ||||||
| 			}).then(subscription => { | 			}) | ||||||
|  | 			.then(subscription => { | ||||||
| 				function encode(buffer: ArrayBuffer | null) { | 				function encode(buffer: ArrayBuffer | null) { | ||||||
| 					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); | 					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); | ||||||
| 				} | 				} | ||||||
|  | @ -44,8 +44,8 @@ export async function initializeSw() { | ||||||
| 				const subscription = await registration.pushManager.getSubscription(); | 				const subscription = await registration.pushManager.getSubscription(); | ||||||
| 				if (subscription) subscription.unsubscribe(); | 				if (subscription) subscription.unsubscribe(); | ||||||
| 			}); | 			}); | ||||||
| 		}); |  | ||||||
| 		} | 		} | ||||||
|  | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -1,107 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Notification composer of Service Worker |  | ||||||
|  */ |  | ||||||
| declare var self: ServiceWorkerGlobalScope; |  | ||||||
| 
 |  | ||||||
| import * as misskey from 'misskey-js'; |  | ||||||
| 
 |  | ||||||
| function getUserName(user: misskey.entities.User): string { |  | ||||||
| 	return user.name || user.username; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> { |  | ||||||
| 	if (!i18n) { |  | ||||||
| 		console.log('no i18n'); |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	switch (type) { |  | ||||||
| 		case 'driveFileCreated': // TODO (Server Side)
 |  | ||||||
| 			return [i18n.t('_notification.fileUploaded'), { |  | ||||||
| 				body: data.name, |  | ||||||
| 				icon: data.url |  | ||||||
| 			}]; |  | ||||||
| 		case 'notification': |  | ||||||
| 			switch (data.type) { |  | ||||||
| 				case 'mention': |  | ||||||
| 					return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'reply': |  | ||||||
| 					return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'renote': |  | ||||||
| 					return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'quote': |  | ||||||
| 					return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'reaction': |  | ||||||
| 					return [`${data.reaction} ${getUserName(data.user)}`, { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'pollVote': |  | ||||||
| 					return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'pollEnded': |  | ||||||
| 					return [i18n.t('_notification.pollEnded'), { |  | ||||||
| 						body: data.note.text, |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'follow': |  | ||||||
| 					return [i18n.t('_notification.youWereFollowed'), { |  | ||||||
| 						body: getUserName(data.user), |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'receiveFollowRequest': |  | ||||||
| 					return [i18n.t('_notification.youReceivedFollowRequest'), { |  | ||||||
| 						body: getUserName(data.user), |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'followRequestAccepted': |  | ||||||
| 					return [i18n.t('_notification.yourFollowRequestAccepted'), { |  | ||||||
| 						body: getUserName(data.user), |  | ||||||
| 						icon: data.user.avatarUrl |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				case 'groupInvited': |  | ||||||
| 					return [i18n.t('_notification.youWereInvitedToGroup'), { |  | ||||||
| 						body: data.group.name |  | ||||||
| 					}]; |  | ||||||
| 
 |  | ||||||
| 				default: |  | ||||||
| 					return null; |  | ||||||
| 			} |  | ||||||
| 		case 'unreadMessagingMessage': |  | ||||||
| 			if (data.groupId === null) { |  | ||||||
| 				return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), { |  | ||||||
| 					icon: data.user.avatarUrl, |  | ||||||
| 					tag: `messaging:user:${data.user.id}` |  | ||||||
| 				}]; |  | ||||||
| 			} |  | ||||||
| 			return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), { |  | ||||||
| 				icon: data.user.avatarUrl, |  | ||||||
| 				tag: `messaging:group:${data.group.id}` |  | ||||||
| 			}]; |  | ||||||
| 		default: |  | ||||||
| 			return null; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,123 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Service Worker |  | ||||||
|  */ |  | ||||||
| declare var self: ServiceWorkerGlobalScope; |  | ||||||
| 
 |  | ||||||
| import { get, set } from 'idb-keyval'; |  | ||||||
| import composeNotification from '@/sw/compose-notification'; |  | ||||||
| import { I18n } from '@/scripts/i18n'; |  | ||||||
| 
 |  | ||||||
| //#region Variables
 |  | ||||||
| const version = _VERSION_; |  | ||||||
| const cacheName = `mk-cache-${version}`; |  | ||||||
| 
 |  | ||||||
| let lang: string; |  | ||||||
| let i18n: I18n<any>; |  | ||||||
| let pushesPool: any[] = []; |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region Startup
 |  | ||||||
| get('lang').then(async prelang => { |  | ||||||
| 	if (!prelang) return; |  | ||||||
| 	lang = prelang; |  | ||||||
| 	return fetchLocale(); |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region Lifecycle: Install
 |  | ||||||
| self.addEventListener('install', ev => { |  | ||||||
| 	self.skipWaiting(); |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region Lifecycle: Activate
 |  | ||||||
| self.addEventListener('activate', ev => { |  | ||||||
| 	ev.waitUntil( |  | ||||||
| 		caches.keys() |  | ||||||
| 			.then(cacheNames => Promise.all( |  | ||||||
| 				cacheNames |  | ||||||
| 					.filter((v) => v !== cacheName) |  | ||||||
| 					.map(name => caches.delete(name)) |  | ||||||
| 			)) |  | ||||||
| 			.then(() => self.clients.claim()) |  | ||||||
| 	); |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region When: Fetching
 |  | ||||||
| self.addEventListener('fetch', ev => { |  | ||||||
| 	// Nothing to do
 |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region When: Caught Notification
 |  | ||||||
| self.addEventListener('push', ev => { |  | ||||||
| 	// クライアント取得
 |  | ||||||
| 	ev.waitUntil(self.clients.matchAll({ |  | ||||||
| 		includeUncontrolled: true |  | ||||||
| 	}).then(async clients => { |  | ||||||
| 		// クライアントがあったらストリームに接続しているということなので通知しない
 |  | ||||||
| 		if (clients.length != 0) return; |  | ||||||
| 
 |  | ||||||
| 		const { type, body } = ev.data?.json(); |  | ||||||
| 
 |  | ||||||
| 		// localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく
 |  | ||||||
| 		if (!i18n) return pushesPool.push({ type, body }); |  | ||||||
| 
 |  | ||||||
| 		const n = await composeNotification(type, body, i18n); |  | ||||||
| 		if (n) return self.registration.showNotification(...n); |  | ||||||
| 	})); |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region When: Caught a message from the client
 |  | ||||||
| self.addEventListener('message', ev => { |  | ||||||
| 	switch(ev.data) { |  | ||||||
| 		case 'clear': |  | ||||||
| 			return; // TODO
 |  | ||||||
| 		default: |  | ||||||
| 			break; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if (typeof ev.data === 'object') { |  | ||||||
| 		// E.g. '[object Array]' → 'array'
 |  | ||||||
| 		const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); |  | ||||||
| 
 |  | ||||||
| 		if (otype === 'object') { |  | ||||||
| 			if (ev.data.msg === 'initialize') { |  | ||||||
| 				lang = ev.data.lang; |  | ||||||
| 				set('lang', lang); |  | ||||||
| 				fetchLocale(); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| //#endregion
 |  | ||||||
| 
 |  | ||||||
| //#region Function: (Re)Load i18n instance
 |  | ||||||
| async function fetchLocale() { |  | ||||||
| 	//#region localeファイルの読み込み
 |  | ||||||
| 	// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
 |  | ||||||
| 	const localeUrl = `/assets/locales/${lang}.${version}.json`; |  | ||||||
| 	let localeRes = await caches.match(localeUrl); |  | ||||||
| 
 |  | ||||||
| 	if (!localeRes) { |  | ||||||
| 		localeRes = await fetch(localeUrl); |  | ||||||
| 		const clone = localeRes?.clone(); |  | ||||||
| 		if (!clone?.clone().ok) return; |  | ||||||
| 
 |  | ||||||
| 		caches.open(cacheName).then(cache => cache.put(localeUrl, clone)); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	i18n = new I18n(await localeRes.json()); |  | ||||||
| 	//#endregion
 |  | ||||||
| 
 |  | ||||||
| 	//#region i18nをきちんと読み込んだ後にやりたい処理
 |  | ||||||
| 	for (const { type, body } of pushesPool) { |  | ||||||
| 		const n = await composeNotification(type, body, i18n); |  | ||||||
| 		if (n) self.registration.showNotification(...n); |  | ||||||
| 	} |  | ||||||
| 	pushesPool = []; |  | ||||||
| 	//#endregion
 |  | ||||||
| } |  | ||||||
| //#endregion
 |  | ||||||
|  | @ -21,6 +21,7 @@ import { popup, popups, pendingApiRequestsCount } from '@/os'; | ||||||
| import { uploads } from '@/scripts/upload'; | import { uploads } from '@/scripts/upload'; | ||||||
| import * as sound from '@/scripts/sound'; | import * as sound from '@/scripts/sound'; | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
|  | import { swInject } from './sw-inject'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  | @ -49,6 +50,11 @@ export default defineComponent({ | ||||||
| 		if ($i) { | 		if ($i) { | ||||||
| 			const connection = stream.useChannel('main', null, 'UI'); | 			const connection = stream.useChannel('main', null, 'UI'); | ||||||
| 			connection.on('notification', onNotification); | 			connection.on('notification', onNotification); | ||||||
|  | 
 | ||||||
|  | 			//#region Listen message from SW | ||||||
|  | 			if ('serviceWorker' in navigator) { | ||||||
|  | 				swInject(); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return { | 		return { | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								packages/client/src/ui/_common_/sw-inject.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								packages/client/src/ui/_common_/sw-inject.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import { inject } from 'vue'; | ||||||
|  | import { post } from '@/os'; | ||||||
|  | import { $i, login } from '@/account'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
|  | import { getAccountFromId } from '@/scripts/get-account-from-id'; | ||||||
|  | import { router } from '@/router'; | ||||||
|  | 
 | ||||||
|  | export function swInject() { | ||||||
|  | 	const navHook = inject('navHook', null); | ||||||
|  | 	const sideViewHook = inject('sideViewHook', null); | ||||||
|  | 
 | ||||||
|  | 	navigator.serviceWorker.addEventListener('message', ev => { | ||||||
|  | 		if (_DEV_) { | ||||||
|  | 			console.log('sw msg', ev.data); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const data = ev.data; // as SwMessage
 | ||||||
|  | 		if (data.type !== 'order') return; | ||||||
|  | 
 | ||||||
|  | 		if (data.loginId !== $i?.id) { | ||||||
|  | 			return getAccountFromId(data.loginId).then(account => { | ||||||
|  | 				if (!account) return; | ||||||
|  | 				return login(account.token, data.url); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		switch (data.order) { | ||||||
|  | 			case 'post': | ||||||
|  | 				return post(data.options); | ||||||
|  | 			case 'push': | ||||||
|  | 				if (router.currentRoute.value.path === data.url) { | ||||||
|  | 					return window.scroll({ top: 0, behavior: 'smooth' }); | ||||||
|  | 				} | ||||||
|  | 				if (navHook) { | ||||||
|  | 					return navHook(data.url); | ||||||
|  | 				} | ||||||
|  | 				if (sideViewHook && defaultStore.state.defaultSideView && data.url !== '/') { | ||||||
|  | 					return sideViewHook(data.url); | ||||||
|  | 				} | ||||||
|  | 				return router.push(data.url); | ||||||
|  | 			default: | ||||||
|  | 				return; | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | @ -28,8 +28,7 @@ | ||||||
| 		], | 		], | ||||||
| 		"lib": [ | 		"lib": [ | ||||||
| 			"esnext", | 			"esnext", | ||||||
| 			"dom", | 			"dom" | ||||||
| 			"webworker" |  | ||||||
| 		] | 		] | ||||||
| 	}, | 	}, | ||||||
| 	"compileOnSave": false, | 	"compileOnSave": false, | ||||||
|  |  | ||||||
|  | @ -37,7 +37,6 @@ const postcss = { | ||||||
| module.exports = { | module.exports = { | ||||||
| 	entry: { | 	entry: { | ||||||
| 		app: './src/init.ts', | 		app: './src/init.ts', | ||||||
| 		sw: './src/sw/sw.ts' |  | ||||||
| 	}, | 	}, | ||||||
| 	module: { | 	module: { | ||||||
| 		rules: [{ | 		rules: [{ | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								packages/sw/.eslintrc.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/sw/.eslintrc.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | module.exports = { | ||||||
|  | 	root: true, | ||||||
|  | 	env: { | ||||||
|  | 		"node": false | ||||||
|  | 	}, | ||||||
|  | 	parserOptions: { | ||||||
|  | 		"parser": "@typescript-eslint/parser", | ||||||
|  | 		tsconfigRootDir: __dirname, | ||||||
|  | 		//project: ['./tsconfig.json'],
 | ||||||
|  | 	}, | ||||||
|  | 	extends: [ | ||||||
|  | 		//"../shared/.eslintrc.js",
 | ||||||
|  | 	], | ||||||
|  | 	globals: { | ||||||
|  | 		"require": false, | ||||||
|  | 		"_DEV_": false, | ||||||
|  | 		"_LANGS_": false, | ||||||
|  | 		"_VERSION_": false, | ||||||
|  | 		"_ENV_": false, | ||||||
|  | 		"_PERF_PREFIX_": false, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								packages/sw/.npmrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/sw/.npmrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | save-exact = true | ||||||
|  | package-lock = false | ||||||
							
								
								
									
										1
									
								
								packages/sw/.yarnrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/sw/.yarnrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | network-timeout 600000 | ||||||
							
								
								
									
										37
									
								
								packages/sw/build.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/sw/build.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | const esbuild = require('esbuild'); | ||||||
|  | const locales = require('../../locales'); | ||||||
|  | const meta = require('../../package.json'); | ||||||
|  | const watch = process.argv[2]?.includes('watch'); | ||||||
|  | 
 | ||||||
|  | console.log('Starting SW building...'); | ||||||
|  | 
 | ||||||
|  | esbuild.build({ | ||||||
|  | 	entryPoints: [ `${__dirname}/src/sw.ts` ], | ||||||
|  | 	bundle: true, | ||||||
|  | 	format: 'esm', | ||||||
|  | 	treeShaking: true, | ||||||
|  | 	minify: process.env.NODE_ENV === 'production', | ||||||
|  | 	absWorkingDir: __dirname, | ||||||
|  | 	outbase: `${__dirname}/src`, | ||||||
|  | 	outdir: `${__dirname}/../../built/_sw_dist_`, | ||||||
|  | 	loader: { | ||||||
|  | 		'.ts': 'ts' | ||||||
|  | 	}, | ||||||
|  | 	tsconfig: `${__dirname}/tsconfig.json`, | ||||||
|  | 	define: { | ||||||
|  | 		_VERSION_: JSON.stringify(meta.version), | ||||||
|  | 		_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), | ||||||
|  | 		_ENV_: JSON.stringify(process.env.NODE_ENV), | ||||||
|  | 		_DEV_: process.env.NODE_ENV !== 'production', | ||||||
|  | 		_PERF_PREFIX_: JSON.stringify('Misskey:'), | ||||||
|  | 	}, | ||||||
|  | 	watch: watch ? { | ||||||
|  | 		onRebuild(error, result) { | ||||||
|  |       if (error) console.error('SW: watch build failed:', error); | ||||||
|  |       else console.log('SW: watch build succeeded:', result); | ||||||
|  | 		}, | ||||||
|  | 	} : false, | ||||||
|  | }).then(result => { | ||||||
|  | 	if (watch) console.log('watching...'); | ||||||
|  | 	else console.log('done,', JSON.stringify(result)); | ||||||
|  | }); | ||||||
							
								
								
									
										17
									
								
								packages/sw/package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/sw/package.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | { | ||||||
|  | 	"private": true, | ||||||
|  | 	"scripts": { | ||||||
|  | 		"watch": "node build.js watch", | ||||||
|  | 		"build": "node build.js", | ||||||
|  | 		"lint": "eslint --quiet src/**/*.{ts}" | ||||||
|  | 	}, | ||||||
|  | 	"resolutions": {}, | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"esbuild": "^0.14.13", | ||||||
|  | 		"idb-keyval": "^6.0.3", | ||||||
|  | 		"misskey-js": "0.0.14" | ||||||
|  | 	}, | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"eslint": "^8.2.0" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								packages/sw/src/filters/user.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/sw/src/filters/user.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | ||||||
|  | import * as misskey from 'misskey-js'; | ||||||
|  | import * as Acct from 'misskey-js/built/acct'; | ||||||
|  | 
 | ||||||
|  | export const acct = (user: misskey.Acct) => { | ||||||
|  | 	return Acct.toString(user); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const userName = (user: misskey.entities.User) => { | ||||||
|  | 	return user.name || user.username; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const userPage = (user: misskey.Acct, path?, absolute = false) => { | ||||||
|  | 	return `${absolute ? origin : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; | ||||||
|  | }; | ||||||
							
								
								
									
										237
									
								
								packages/sw/src/scripts/create-notification.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								packages/sw/src/scripts/create-notification.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,237 @@ | ||||||
|  | /* | ||||||
|  |  * Notification manager for SW | ||||||
|  |  */ | ||||||
|  | declare var self: ServiceWorkerGlobalScope; | ||||||
|  | 
 | ||||||
|  | import { swLang } from '@/scripts/lang'; | ||||||
|  | import { cli } from '@/scripts/operations'; | ||||||
|  | import { pushNotificationDataMap } from '@/types'; | ||||||
|  | import getUserName from '@/scripts/get-user-name'; | ||||||
|  | import { I18n } from '@/scripts/i18n'; | ||||||
|  | import { getAccountFromId } from '@/scripts/get-account-from-id'; | ||||||
|  | 
 | ||||||
|  | export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) { | ||||||
|  | 	const n = await composeNotification(data); | ||||||
|  | 
 | ||||||
|  | 	if (n) { | ||||||
|  | 		return self.registration.showNotification(...n); | ||||||
|  | 	} else { | ||||||
|  | 		console.error('Could not compose notification', data); | ||||||
|  | 		return createEmptyNotification(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function composeNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]): Promise<[string, NotificationOptions] | null> { | ||||||
|  | 	if (!swLang.i18n) swLang.fetchLocale(); | ||||||
|  | 	const i18n = await swLang.i18n as I18n<any>; | ||||||
|  | 	const { t } = i18n; | ||||||
|  | 	switch (data.type) { | ||||||
|  | 		/* | ||||||
|  | 		case 'driveFileCreated': // TODO (Server Side)
 | ||||||
|  | 			return [t('_notification.fileUploaded'), { | ||||||
|  | 				body: body.name, | ||||||
|  | 				icon: body.url, | ||||||
|  | 				data | ||||||
|  | 			}]; | ||||||
|  | 		*/ | ||||||
|  | 		case 'notification': | ||||||
|  | 			switch (data.body.type) { | ||||||
|  | 				case 'follow': | ||||||
|  | 					// users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用
 | ||||||
|  | 					const account = await getAccountFromId(data.userId); | ||||||
|  | 					if (!account) return null; | ||||||
|  | 					const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token); | ||||||
|  | 					return [t('_notification.youWereFollowed'), { | ||||||
|  | 						body: getUserName(data.body.user), | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: userDetail.isFollowing ? [] : [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'follow', | ||||||
|  | 								title: t('_notification._actions.followBack') | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'mention': | ||||||
|  | 					return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'reply', | ||||||
|  | 								title: t('_notification._actions.reply') | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'reply': | ||||||
|  | 					return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'reply', | ||||||
|  | 								title: t('_notification._actions.reply') | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'renote': | ||||||
|  | 					return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'showUser', | ||||||
|  | 								title: getUserName(data.body.user) | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'quote': | ||||||
|  | 					return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'reply', | ||||||
|  | 								title: t('_notification._actions.reply') | ||||||
|  | 							}, | ||||||
|  | 							...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'renote', | ||||||
|  | 								title: t('_notification._actions.renote') | ||||||
|  | 							} | ||||||
|  | 							] : []) | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'reaction': | ||||||
|  | 					return [`${data.body.reaction} ${getUserName(data.body.user)}`, { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'showUser', | ||||||
|  | 								title: getUserName(data.body.user) | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'pollVote': | ||||||
|  | 					return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'pollEnded': | ||||||
|  | 					return [t('_notification.pollEnded'), { | ||||||
|  | 						body: data.body.note.text || '', | ||||||
|  | 						data, | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'receiveFollowRequest': | ||||||
|  | 					return [t('_notification.youReceivedFollowRequest'), { | ||||||
|  | 						body: getUserName(data.body.user), | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'accept', | ||||||
|  | 								title: t('accept') | ||||||
|  | 							}, | ||||||
|  | 							{ | ||||||
|  | 								action: 'reject', | ||||||
|  | 								title: t('reject') | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'followRequestAccepted': | ||||||
|  | 					return [t('_notification.yourFollowRequestAccepted'), { | ||||||
|  | 						body: getUserName(data.body.user), | ||||||
|  | 						icon: data.body.user.avatarUrl, | ||||||
|  | 						data, | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'groupInvited': | ||||||
|  | 					return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), { | ||||||
|  | 						body: data.body.invitation.group.name, | ||||||
|  | 						data, | ||||||
|  | 						actions: [ | ||||||
|  | 							{ | ||||||
|  | 								action: 'accept', | ||||||
|  | 								title: t('accept') | ||||||
|  | 							}, | ||||||
|  | 							{ | ||||||
|  | 								action: 'reject', | ||||||
|  | 								title: t('reject') | ||||||
|  | 							} | ||||||
|  | 						], | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
|  | 				case 'app': | ||||||
|  | 						return [data.body.header || data.body.body, { | ||||||
|  | 							body: data.body.header && data.body.body, | ||||||
|  | 							icon: data.body.icon, | ||||||
|  | 							data | ||||||
|  | 						}]; | ||||||
|  | 
 | ||||||
|  | 				default: | ||||||
|  | 					return null; | ||||||
|  | 			} | ||||||
|  | 		case 'unreadMessagingMessage': | ||||||
|  | 			if (data.body.groupId === null) { | ||||||
|  | 				return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { | ||||||
|  | 					icon: data.body.user.avatarUrl, | ||||||
|  | 					tag: `messaging:user:${data.body.userId}`, | ||||||
|  | 					data, | ||||||
|  | 					renotify: true, | ||||||
|  | 				}]; | ||||||
|  | 			} | ||||||
|  | 			return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { | ||||||
|  | 				icon: data.body.user.avatarUrl, | ||||||
|  | 				tag: `messaging:group:${data.body.groupId}`, | ||||||
|  | 				data, | ||||||
|  | 				renotify: true, | ||||||
|  | 			}]; | ||||||
|  | 		default: | ||||||
|  | 			return null; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createEmptyNotification() { | ||||||
|  | 	return new Promise<void>(async res => { | ||||||
|  | 		if (!swLang.i18n) swLang.fetchLocale(); | ||||||
|  | 		const i18n = await swLang.i18n as I18n<any>; | ||||||
|  | 		const { t } = i18n; | ||||||
|  | 	 | ||||||
|  | 		await self.registration.showNotification( | ||||||
|  | 			t('_notification.emptyPushNotificationMessage'), | ||||||
|  | 			{ | ||||||
|  | 				silent: true, | ||||||
|  | 				tag: 'read_notification', | ||||||
|  | 			} | ||||||
|  | 		); | ||||||
|  | 
 | ||||||
|  | 		res(); | ||||||
|  | 
 | ||||||
|  | 		setTimeout(async () => { | ||||||
|  | 			for (const n of | ||||||
|  | 				[ | ||||||
|  | 					...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })), | ||||||
|  | 					...(await self.registration.getNotifications({ tag: 'read_notification' })) | ||||||
|  | 				] | ||||||
|  | 			) { | ||||||
|  | 				n.close(); | ||||||
|  | 			} | ||||||
|  | 		}, 1000); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								packages/sw/src/scripts/get-account-from-id.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/sw/src/scripts/get-account-from-id.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import { get } from 'idb-keyval'; | ||||||
|  | 
 | ||||||
|  | export async function getAccountFromId(id: string) { | ||||||
|  | 	const accounts = await get('accounts') as { token: string; id: string; }[]; | ||||||
|  | 	if (!accounts) console.log('Accounts are not recorded'); | ||||||
|  | 	return accounts.find(e => e.id === id); | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								packages/sw/src/scripts/get-user-name.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/sw/src/scripts/get-user-name.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export default function(user: { name?: string | null, username: string }): string { | ||||||
|  | 	return user.name || user.username; | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								packages/sw/src/scripts/i18n.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								packages/sw/src/scripts/i18n.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | export class I18n<T extends Record<string, any>> { | ||||||
|  | 	public ts: T; | ||||||
|  | 
 | ||||||
|  | 	constructor(locale: T) { | ||||||
|  | 		this.ts = locale; | ||||||
|  | 
 | ||||||
|  | 		//#region BIND
 | ||||||
|  | 		this.t = this.t.bind(this); | ||||||
|  | 		//#endregion
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// string にしているのは、ドット区切りでのパス指定を許可するため
 | ||||||
|  | 	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
 | ||||||
|  | 	public t(key: string, args?: Record<string, string>): string { | ||||||
|  | 		try { | ||||||
|  | 			let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; | ||||||
|  | 
 | ||||||
|  | 			if (args) { | ||||||
|  | 				for (const [k, v] of Object.entries(args)) { | ||||||
|  | 					str = str.replace(`{${k}}`, v); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return str; | ||||||
|  | 		} catch (err) { | ||||||
|  | 			console.warn(`missing localization '${key}'`); | ||||||
|  | 			return key; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								packages/sw/src/scripts/lang.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								packages/sw/src/scripts/lang.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | /* | ||||||
|  |  * Language manager for SW | ||||||
|  |  */ | ||||||
|  | declare var self: ServiceWorkerGlobalScope; | ||||||
|  | 
 | ||||||
|  | import { get, set } from 'idb-keyval'; | ||||||
|  | import { I18n } from '@/scripts/i18n'; | ||||||
|  | 
 | ||||||
|  | class SwLang { | ||||||
|  | 	public cacheName = `mk-cache-${_VERSION_}`; | ||||||
|  | 
 | ||||||
|  | 	public lang: Promise<string> = get('lang').then(async prelang => { | ||||||
|  | 		if (!prelang) return 'en-US'; | ||||||
|  | 		return prelang; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	public setLang(newLang: string) { | ||||||
|  | 		this.lang = Promise.resolve(newLang); | ||||||
|  | 		set('lang', newLang); | ||||||
|  | 		return this.fetchLocale(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public i18n: Promise<I18n<any>> | null = null; | ||||||
|  | 
 | ||||||
|  | 	public fetchLocale() { | ||||||
|  | 		return this.i18n = this._fetch(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private async _fetch() { | ||||||
|  | 		// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
 | ||||||
|  | 		const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`; | ||||||
|  | 		let localeRes = await caches.match(localeUrl); | ||||||
|  | 
 | ||||||
|  | 		// _DEV_がtrueの場合は常に最新化
 | ||||||
|  | 		if (!localeRes || _DEV_) { | ||||||
|  | 			localeRes = await fetch(localeUrl); | ||||||
|  | 			const clone = localeRes?.clone(); | ||||||
|  | 			if (!clone?.clone().ok) Error('locale fetching error'); | ||||||
|  | 
 | ||||||
|  | 			caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return new I18n(await localeRes.json()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const swLang = new SwLang(); | ||||||
							
								
								
									
										11
									
								
								packages/sw/src/scripts/login-id.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/sw/src/scripts/login-id.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | export function getUrlWithLoginId(url: string, loginId: string) { | ||||||
|  | 	const u = new URL(url, origin); | ||||||
|  | 	u.searchParams.append('loginId', loginId); | ||||||
|  | 	return u.toString(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getUrlWithoutLoginId(url: string) { | ||||||
|  | 	const u = new URL(url); | ||||||
|  | 	u.searchParams.delete('loginId'); | ||||||
|  | 	return u.toString(); | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								packages/sw/src/scripts/notification-read.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								packages/sw/src/scripts/notification-read.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | declare var self: ServiceWorkerGlobalScope; | ||||||
|  | 
 | ||||||
|  | import { get } from 'idb-keyval'; | ||||||
|  | import { pushNotificationDataMap } from '@/types'; | ||||||
|  | import { api } from '@/scripts/operations'; | ||||||
|  | 
 | ||||||
|  | type Accounts = { | ||||||
|  | 	[x: string]: { | ||||||
|  | 		queue: string[], | ||||||
|  | 		timeout: number | null | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | class SwNotificationReadManager { | ||||||
|  | 	private accounts: Accounts = {}; | ||||||
|  | 
 | ||||||
|  | 	public async construct() { | ||||||
|  | 		const accounts = await get('accounts'); | ||||||
|  | 		if (!accounts) Error('Accounts are not recorded'); | ||||||
|  | 
 | ||||||
|  | 		this.accounts = accounts.reduce((acc, e) => { | ||||||
|  | 			acc[e.id] = { | ||||||
|  | 				queue: [], | ||||||
|  | 				timeout: null | ||||||
|  | 			}; | ||||||
|  | 			return acc; | ||||||
|  | 		}, {} as Accounts); | ||||||
|  | 
 | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// プッシュ通知の既読をサーバーに送信
 | ||||||
|  | 	public async read<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) { | ||||||
|  | 		if (data.type !== 'notification' || !(data.userId in this.accounts)) return; | ||||||
|  | 
 | ||||||
|  | 		const account = this.accounts[data.userId]; | ||||||
|  | 
 | ||||||
|  | 		account.queue.push(data.body.id as string); | ||||||
|  | 
 | ||||||
|  | 		// 最後の呼び出しから200ms待ってまとめて処理する
 | ||||||
|  | 		if (account.timeout) clearTimeout(account.timeout); | ||||||
|  | 		account.timeout = setTimeout(() => { | ||||||
|  | 			account.timeout = null; | ||||||
|  | 
 | ||||||
|  | 			api('notifications/read', data.userId, { notificationIds: account.queue }); | ||||||
|  | 		}, 200); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const swNotificationRead = (new SwNotificationReadManager()).construct(); | ||||||
							
								
								
									
										70
									
								
								packages/sw/src/scripts/operations.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								packages/sw/src/scripts/operations.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | /* | ||||||
|  |  * Operations | ||||||
|  |  * 各種操作 | ||||||
|  |  */ | ||||||
|  | declare var self: ServiceWorkerGlobalScope; | ||||||
|  | 
 | ||||||
|  | import * as Misskey from 'misskey-js'; | ||||||
|  | import { SwMessage, swMessageOrderType } from '@/types'; | ||||||
|  | import { acct as getAcct } from '@/filters/user'; | ||||||
|  | import { getAccountFromId } from '@/scripts/get-account-from-id'; | ||||||
|  | import { getUrlWithLoginId } from '@/scripts/login-id'; | ||||||
|  | 
 | ||||||
|  | export const cli = new Misskey.api.APIClient({ origin, fetch: (...args) => fetch(...args) }); | ||||||
|  | 
 | ||||||
|  | export async function api<E extends keyof Misskey.Endpoints>(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) { | ||||||
|  | 	const account = await getAccountFromId(userId); | ||||||
|  | 	if (!account) return; | ||||||
|  | 
 | ||||||
|  | 	return cli.request(endpoint, options, account.token); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // rendered acctからユーザーを開く
 | ||||||
|  | export function openUser(acct: string, loginId: string) { | ||||||
|  | 	return openClient('push', `/@${acct}`, loginId, { acct }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // noteIdからノートを開く
 | ||||||
|  | export function openNote(noteId: string, loginId: string) { | ||||||
|  | 	return openClient('push', `/notes/${noteId}`, loginId, { noteId }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function openChat(body: any, loginId: string) { | ||||||
|  | 	if (body.groupId === null) { | ||||||
|  | 		return openClient('push', `/my/messaging/${getAcct(body.user)}`, loginId, { body }); | ||||||
|  | 	} else { | ||||||
|  | 		return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body }); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // post-formのオプションから投稿フォームを開く
 | ||||||
|  | export async function openPost(options: any, loginId: string) { | ||||||
|  | 	// クエリを作成しておく
 | ||||||
|  | 	let url = `/share?`; | ||||||
|  | 	if (options.initialText) url += `text=${options.initialText}&`; | ||||||
|  | 	if (options.reply) url += `replyId=${options.reply.id}&`; | ||||||
|  | 	if (options.renote) url += `renoteId=${options.renote.id}&`; | ||||||
|  | 
 | ||||||
|  | 	return openClient('post', url, loginId, { options }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) { | ||||||
|  | 	const client = await findClient(); | ||||||
|  | 
 | ||||||
|  | 	if (client) { | ||||||
|  | 		client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); | ||||||
|  | 		return client; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return self.clients.openWindow(getUrlWithLoginId(url, loginId)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function findClient() { | ||||||
|  | 	const clients = await self.clients.matchAll({ | ||||||
|  | 		type: 'window' | ||||||
|  | 	}); | ||||||
|  | 	for (const c of clients) { | ||||||
|  | 		if (c.url.indexOf('?zen') < 0) return c; | ||||||
|  | 	} | ||||||
|  | 	return null; | ||||||
|  | } | ||||||
							
								
								
									
										200
									
								
								packages/sw/src/sw.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								packages/sw/src/sw.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,200 @@ | ||||||
|  | declare var self: ServiceWorkerGlobalScope; | ||||||
|  | 
 | ||||||
|  | import { createEmptyNotification, createNotification } from '@/scripts/create-notification'; | ||||||
|  | import { swLang } from '@/scripts/lang'; | ||||||
|  | import { swNotificationRead } from '@/scripts/notification-read'; | ||||||
|  | import { pushNotificationDataMap } from '@/types'; | ||||||
|  | import * as swos from '@/scripts/operations'; | ||||||
|  | import { acct as getAcct } from '@/filters/user'; | ||||||
|  | 
 | ||||||
|  | self.addEventListener('install', ev => { | ||||||
|  | 	ev.waitUntil(self.skipWaiting()); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('activate', ev => { | ||||||
|  | 	ev.waitUntil( | ||||||
|  | 		caches.keys() | ||||||
|  | 			.then(cacheNames => Promise.all( | ||||||
|  | 				cacheNames | ||||||
|  | 					.filter((v) => v !== swLang.cacheName) | ||||||
|  | 					.map(name => caches.delete(name)) | ||||||
|  | 			)) | ||||||
|  | 			.then(() => self.clients.claim()) | ||||||
|  | 	); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('fetch', ev => { | ||||||
|  | 	ev.respondWith( | ||||||
|  | 		fetch(ev.request) | ||||||
|  | 		.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })) | ||||||
|  | 	); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('push', ev => { | ||||||
|  | 	// クライアント取得
 | ||||||
|  | 	ev.waitUntil(self.clients.matchAll({ | ||||||
|  | 		includeUncontrolled: true, | ||||||
|  | 		type: 'window' | ||||||
|  | 	}).then(async <K extends keyof pushNotificationDataMap>(clients: readonly WindowClient[]) => { | ||||||
|  | 		const data: pushNotificationDataMap[K] = ev.data?.json(); | ||||||
|  | 
 | ||||||
|  | 		switch (data.type) { | ||||||
|  | 			// case 'driveFileCreated':
 | ||||||
|  | 			case 'notification': | ||||||
|  | 			case 'unreadMessagingMessage': | ||||||
|  | 				// クライアントがあったらストリームに接続しているということなので通知しない
 | ||||||
|  | 				if (clients.length != 0) return; | ||||||
|  | 				return createNotification(data); | ||||||
|  | 			case 'readAllNotifications': | ||||||
|  | 				for (const n of await self.registration.getNotifications()) { | ||||||
|  | 					if (n?.data?.type === 'notification') n.close(); | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
|  | 			case 'readAllMessagingMessages': | ||||||
|  | 				for (const n of await self.registration.getNotifications()) { | ||||||
|  | 					if (n?.data?.type === 'unreadMessagingMessage') n.close(); | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
|  | 			case 'readNotifications': | ||||||
|  | 				for (const n of await self.registration.getNotifications()) { | ||||||
|  | 					if (data.body?.notificationIds?.includes(n.data.body.id)) { | ||||||
|  | 						n.close(); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
|  | 			case 'readAllMessagingMessagesOfARoom': | ||||||
|  | 				for (const n of await self.registration.getNotifications()) { | ||||||
|  | 					if (n.data.type === 'unreadMessagingMessage' | ||||||
|  | 						&& ('userId' in data.body | ||||||
|  | 							? data.body.userId === n.data.body.userId | ||||||
|  | 							: data.body.groupId === n.data.body.groupId) | ||||||
|  | 						) { | ||||||
|  | 							n.close(); | ||||||
|  | 						} | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return createEmptyNotification(); | ||||||
|  | 	})); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('notificationclick', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { | ||||||
|  | 	ev.waitUntil((async () => { | ||||||
|  | 		if (_DEV_) { | ||||||
|  | 			console.log('notificationclick', ev.action, ev.notification.data); | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 		const { action, notification } = ev; | ||||||
|  | 		const data: pushNotificationDataMap[K] = notification.data; | ||||||
|  | 		const { userId: id } = data; | ||||||
|  | 		let client: WindowClient | null = null; | ||||||
|  | 	 | ||||||
|  | 		switch (data.type) { | ||||||
|  | 			case 'notification': | ||||||
|  | 				switch (action) { | ||||||
|  | 					case 'follow': | ||||||
|  | 						if ('userId' in data.body) await swos.api('following/create', id, { userId: data.body.userId }); | ||||||
|  | 						break; | ||||||
|  | 					case 'showUser': | ||||||
|  | 						if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), id); | ||||||
|  | 						break; | ||||||
|  | 					case 'reply': | ||||||
|  | 						if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, id); | ||||||
|  | 						break; | ||||||
|  | 					case 'renote': | ||||||
|  | 						if ('note' in data.body) await swos.api('notes/create', id, { renoteId: data.body.note.id }); | ||||||
|  | 						break; | ||||||
|  | 					case 'accept': | ||||||
|  | 						switch (data.body.type) { | ||||||
|  | 							case 'receiveFollowRequest': | ||||||
|  | 								await swos.api('following/requests/accept', id, { userId: data.body.userId }); | ||||||
|  | 								break; | ||||||
|  | 							case 'groupInvited': | ||||||
|  | 								await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id }); | ||||||
|  | 								break; | ||||||
|  | 						} | ||||||
|  | 						break; | ||||||
|  | 					case 'reject': | ||||||
|  | 						switch (data.body.type) { | ||||||
|  | 							case 'receiveFollowRequest': | ||||||
|  | 								await swos.api('following/requests/reject', id, { userId: data.body.userId }); | ||||||
|  | 								break; | ||||||
|  | 							case 'groupInvited': | ||||||
|  | 								await swos.api('users/groups/invitations/reject', id, { invitationId: data.body.invitation.id }); | ||||||
|  | 								break; | ||||||
|  | 						} | ||||||
|  | 						break; | ||||||
|  | 					case 'showFollowRequests': | ||||||
|  | 						client = await swos.openClient('push', '/my/follow-requests', id); | ||||||
|  | 						break; | ||||||
|  | 					default: | ||||||
|  | 						switch (data.body.type) { | ||||||
|  | 							case 'receiveFollowRequest': | ||||||
|  | 								client = await swos.openClient('push', '/my/follow-requests', id); | ||||||
|  | 								break; | ||||||
|  | 							case 'groupInvited': | ||||||
|  | 								client = await swos.openClient('push', '/my/groups', id); | ||||||
|  | 								break; | ||||||
|  | 							case 'reaction': | ||||||
|  | 								client = await swos.openNote(data.body.note.id, id); | ||||||
|  | 								break; | ||||||
|  | 							default: | ||||||
|  | 								if ('note' in data.body) { | ||||||
|  | 									client = await swos.openNote(data.body.note.id, id); | ||||||
|  | 								} else if ('user' in data.body) { | ||||||
|  | 									client = await swos.openUser(getAcct(data.body.user), id); | ||||||
|  | 								} | ||||||
|  | 								break; | ||||||
|  | 						} | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
|  | 			case 'unreadMessagingMessage': | ||||||
|  | 				client = await swos.openChat(data.body, id); | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 		if (client) { | ||||||
|  | 			client.focus(); | ||||||
|  | 		} | ||||||
|  | 		if (data.type === 'notification') { | ||||||
|  | 			swNotificationRead.then(that => that.read(data)); | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 		notification.close(); | ||||||
|  | 	 | ||||||
|  | 	})()); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('notificationclose', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { | ||||||
|  | 	const data: pushNotificationDataMap[K] = ev.notification.data; | ||||||
|  | 
 | ||||||
|  | 	if (data.type === 'notification') { | ||||||
|  | 		swNotificationRead.then(that => that.read(data)); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { | ||||||
|  | 	ev.waitUntil((async () => { | ||||||
|  | 		switch (ev.data) { | ||||||
|  | 			case 'clear': | ||||||
|  | 				// Cache Storage全削除
 | ||||||
|  | 				await caches.keys() | ||||||
|  | 					.then(cacheNames => Promise.all( | ||||||
|  | 						cacheNames.map(name => caches.delete(name)) | ||||||
|  | 					)); | ||||||
|  | 				return; // TODO
 | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 		if (typeof ev.data === 'object') { | ||||||
|  | 			// E.g. '[object Array]' → 'array'
 | ||||||
|  | 			const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); | ||||||
|  | 	 | ||||||
|  | 			if (otype === 'object') { | ||||||
|  | 				if (ev.data.msg === 'initialize') { | ||||||
|  | 					swLang.setLang(ev.data.lang); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	})()); | ||||||
|  | }); | ||||||
							
								
								
									
										31
									
								
								packages/sw/src/types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/sw/src/types.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | import * as Misskey from 'misskey-js'; | ||||||
|  | 
 | ||||||
|  | export type swMessageOrderType = 'post' | 'push'; | ||||||
|  | 
 | ||||||
|  | export type SwMessage = { | ||||||
|  | 	type: 'order'; | ||||||
|  | 	order: swMessageOrderType; | ||||||
|  | 	loginId: string; | ||||||
|  | 	url: string; | ||||||
|  | 	[x: string]: any; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Defined also @/services/push-notification.ts#L7-L14
 | ||||||
|  | type pushNotificationDataSourceMap = { | ||||||
|  | 	notification: Misskey.entities.Notification; | ||||||
|  | 	unreadMessagingMessage: Misskey.entities.MessagingMessage; | ||||||
|  | 	readNotifications: { notificationIds: string[] }; | ||||||
|  | 	readAllNotifications: undefined; | ||||||
|  | 	readAllMessagingMessages: undefined; | ||||||
|  | 	readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = { | ||||||
|  | 	type: K; | ||||||
|  | 	body: pushNotificationDataSourceMap[K]; | ||||||
|  | 	userId: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type pushNotificationDataMap = { | ||||||
|  | 	[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>; | ||||||
|  | }; | ||||||
							
								
								
									
										39
									
								
								packages/sw/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/sw/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | { | ||||||
|  | 	"compilerOptions": { | ||||||
|  | 		"allowJs": true, | ||||||
|  | 		"noEmitOnError": false, | ||||||
|  | 		"noImplicitAny": false, | ||||||
|  | 		"noImplicitReturns": true, | ||||||
|  | 		"noUnusedParameters": false, | ||||||
|  | 		"noUnusedLocals": true, | ||||||
|  | 		"noFallthroughCasesInSwitch": true, | ||||||
|  | 		"declaration": false, | ||||||
|  | 		"sourceMap": false, | ||||||
|  | 		"target": "es2017", | ||||||
|  | 		"module": "esnext", | ||||||
|  | 		"moduleResolution": "node", | ||||||
|  | 		"removeComments": false, | ||||||
|  | 		"noLib": false, | ||||||
|  | 		"strict": true, | ||||||
|  | 		"strictNullChecks": true, | ||||||
|  | 		"experimentalDecorators": true, | ||||||
|  | 		"resolveJsonModule": true, | ||||||
|  | 		"isolatedModules": true, | ||||||
|  | 		"baseUrl": ".", | ||||||
|  | 		"paths": { | ||||||
|  | 			"@/*": ["./src/*"], | ||||||
|  | 		}, | ||||||
|  | 		"typeRoots": [ | ||||||
|  | 			"node_modules/@types", | ||||||
|  | 			"@types", | ||||||
|  | 		], | ||||||
|  | 		"lib": [ | ||||||
|  | 			"esnext", | ||||||
|  | 			"webworker" | ||||||
|  | 		] | ||||||
|  | 	}, | ||||||
|  | 	"compileOnSave": false, | ||||||
|  | 	"include": [ | ||||||
|  | 		"./**/*.ts" | ||||||
|  | 	] | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								packages/sw/webpack.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								packages/sw/webpack.config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | /** | ||||||
|  |  * webpack configuration | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | const fs = require('fs'); | ||||||
|  | const webpack = require('webpack'); | ||||||
|  | 
 | ||||||
|  | class WebpackOnBuildPlugin { | ||||||
|  | 	constructor(callback) { | ||||||
|  | 		this.callback = callback; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	apply(compiler) { | ||||||
|  | 		compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const isProduction = process.env.NODE_ENV === 'production'; | ||||||
|  | 
 | ||||||
|  | const locales = require('../../locales'); | ||||||
|  | const meta = require('../../package.json'); | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	target: 'webworker', | ||||||
|  | 	entry: { | ||||||
|  | 		['sw-lib']: './src/lib.ts' | ||||||
|  | 	}, | ||||||
|  | 	module: { | ||||||
|  | 		rules: [{ | ||||||
|  | 			test: /\.ts$/, | ||||||
|  | 			exclude: /node_modules/, | ||||||
|  | 			use: [{ | ||||||
|  | 				loader: 'ts-loader', | ||||||
|  | 				options: { | ||||||
|  | 					happyPackMode: true, | ||||||
|  | 					transpileOnly: true, | ||||||
|  | 					configFile: __dirname + '/tsconfig.json', | ||||||
|  | 				} | ||||||
|  | 			}] | ||||||
|  | 		}] | ||||||
|  | 	}, | ||||||
|  | 	plugins: [ | ||||||
|  | 		new webpack.ProgressPlugin({}), | ||||||
|  | 		new webpack.DefinePlugin({ | ||||||
|  | 			_VERSION_: JSON.stringify(meta.version), | ||||||
|  | 			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), | ||||||
|  | 			_ENV_: JSON.stringify(process.env.NODE_ENV), | ||||||
|  | 			_DEV_: process.env.NODE_ENV !== 'production', | ||||||
|  | 			_PERF_PREFIX_: JSON.stringify('Misskey:'), | ||||||
|  | 		}), | ||||||
|  | 	], | ||||||
|  | 	output: { | ||||||
|  | 		path: __dirname + '/../../built/_sw_dist_', | ||||||
|  | 		filename: `[name].js`, | ||||||
|  | 		publicPath: `/`, | ||||||
|  | 		pathinfo: false, | ||||||
|  | 	}, | ||||||
|  | 	resolve: { | ||||||
|  | 		extensions: [ | ||||||
|  | 			'.js', '.ts', '.json' | ||||||
|  | 		], | ||||||
|  | 		alias: { | ||||||
|  | 			'@': __dirname + '/src/', | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	resolveLoader: { | ||||||
|  | 		modules: ['node_modules'] | ||||||
|  | 	}, | ||||||
|  | 	devtool: false, //'source-map',
 | ||||||
|  | 	mode: isProduction ? 'production' : 'development' | ||||||
|  | }; | ||||||
							
								
								
									
										710
									
								
								packages/sw/yarn.lock
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										710
									
								
								packages/sw/yarn.lock
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,710 @@ | ||||||
|  | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||||||
|  | # yarn lockfile v1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | "@eslint/eslintrc@^1.0.5": | ||||||
|  |   version "1.0.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" | ||||||
|  |   integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== | ||||||
|  |   dependencies: | ||||||
|  |     ajv "^6.12.4" | ||||||
|  |     debug "^4.3.2" | ||||||
|  |     espree "^9.2.0" | ||||||
|  |     globals "^13.9.0" | ||||||
|  |     ignore "^4.0.6" | ||||||
|  |     import-fresh "^3.2.1" | ||||||
|  |     js-yaml "^4.1.0" | ||||||
|  |     minimatch "^3.0.4" | ||||||
|  |     strip-json-comments "^3.1.1" | ||||||
|  | 
 | ||||||
|  | "@humanwhocodes/config-array@^0.9.2": | ||||||
|  |   version "0.9.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" | ||||||
|  |   integrity sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@humanwhocodes/object-schema" "^1.2.1" | ||||||
|  |     debug "^4.1.1" | ||||||
|  |     minimatch "^3.0.4" | ||||||
|  | 
 | ||||||
|  | "@humanwhocodes/object-schema@^1.2.1": | ||||||
|  |   version "1.2.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" | ||||||
|  |   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== | ||||||
|  | 
 | ||||||
|  | acorn-jsx@^5.3.1: | ||||||
|  |   version "5.3.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" | ||||||
|  |   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== | ||||||
|  | 
 | ||||||
|  | acorn@^8.7.0: | ||||||
|  |   version "8.7.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" | ||||||
|  |   integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== | ||||||
|  | 
 | ||||||
|  | ajv@^6.10.0, ajv@^6.12.4: | ||||||
|  |   version "6.12.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" | ||||||
|  |   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== | ||||||
|  |   dependencies: | ||||||
|  |     fast-deep-equal "^3.1.1" | ||||||
|  |     fast-json-stable-stringify "^2.0.0" | ||||||
|  |     json-schema-traverse "^0.4.1" | ||||||
|  |     uri-js "^4.2.2" | ||||||
|  | 
 | ||||||
|  | ansi-regex@^5.0.1: | ||||||
|  |   version "5.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" | ||||||
|  |   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== | ||||||
|  | 
 | ||||||
|  | ansi-styles@^4.1.0: | ||||||
|  |   version "4.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" | ||||||
|  |   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== | ||||||
|  |   dependencies: | ||||||
|  |     color-convert "^2.0.1" | ||||||
|  | 
 | ||||||
|  | argparse@^2.0.1: | ||||||
|  |   version "2.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | ||||||
|  |   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== | ||||||
|  | 
 | ||||||
|  | autobind-decorator@^2.4.0: | ||||||
|  |   version "2.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c" | ||||||
|  |   integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw== | ||||||
|  | 
 | ||||||
|  | balanced-match@^1.0.0: | ||||||
|  |   version "1.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" | ||||||
|  |   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== | ||||||
|  | 
 | ||||||
|  | brace-expansion@^1.1.7: | ||||||
|  |   version "1.1.11" | ||||||
|  |   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" | ||||||
|  |   integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== | ||||||
|  |   dependencies: | ||||||
|  |     balanced-match "^1.0.0" | ||||||
|  |     concat-map "0.0.1" | ||||||
|  | 
 | ||||||
|  | callsites@^3.0.0: | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" | ||||||
|  |   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== | ||||||
|  | 
 | ||||||
|  | chalk@^4.0.0: | ||||||
|  |   version "4.1.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" | ||||||
|  |   integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== | ||||||
|  |   dependencies: | ||||||
|  |     ansi-styles "^4.1.0" | ||||||
|  |     supports-color "^7.1.0" | ||||||
|  | 
 | ||||||
|  | color-convert@^2.0.1: | ||||||
|  |   version "2.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" | ||||||
|  |   integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== | ||||||
|  |   dependencies: | ||||||
|  |     color-name "~1.1.4" | ||||||
|  | 
 | ||||||
|  | color-name@~1.1.4: | ||||||
|  |   version "1.1.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" | ||||||
|  |   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | ||||||
|  | 
 | ||||||
|  | concat-map@0.0.1: | ||||||
|  |   version "0.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" | ||||||
|  |   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= | ||||||
|  | 
 | ||||||
|  | cross-spawn@^7.0.2: | ||||||
|  |   version "7.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" | ||||||
|  |   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== | ||||||
|  |   dependencies: | ||||||
|  |     path-key "^3.1.0" | ||||||
|  |     shebang-command "^2.0.0" | ||||||
|  |     which "^2.0.1" | ||||||
|  | 
 | ||||||
|  | debug@^4.1.1, debug@^4.3.2: | ||||||
|  |   version "4.3.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" | ||||||
|  |   integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== | ||||||
|  |   dependencies: | ||||||
|  |     ms "2.1.2" | ||||||
|  | 
 | ||||||
|  | deep-is@^0.1.3: | ||||||
|  |   version "0.1.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" | ||||||
|  |   integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== | ||||||
|  | 
 | ||||||
|  | doctrine@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" | ||||||
|  |   integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== | ||||||
|  |   dependencies: | ||||||
|  |     esutils "^2.0.2" | ||||||
|  | 
 | ||||||
|  | esbuild-android-arm64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.17.tgz#7216810cb8d5b8cd03ce70bdc241dcdd90c34755" | ||||||
|  |   integrity sha512-y7EJm8ADC9qKbo/dJ2zBXwNdIILJ76tTv7JDGvOkbLT8HJXIsgbpa0NJk7iFhyvP4GpsYvXTbvEQNn0DhyBhLA== | ||||||
|  | 
 | ||||||
|  | esbuild-darwin-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.17.tgz#1419e020f41814f8a74ce92b2dcab29a6d47e510" | ||||||
|  |   integrity sha512-V2JAP8yyVbW6qR4SVXsEDqRicYM0x5niUuB05IFiE5itPI45k8j2dA2l+DtirR2SGXr+LEqgX347+2VA6eyTiA== | ||||||
|  | 
 | ||||||
|  | esbuild-darwin-arm64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.17.tgz#95acf1022066d48346a63ffc5e4d36a07b83c9b0" | ||||||
|  |   integrity sha512-ENkSKpjF4SImyA2TdHhKiZqtYc1DkMykICe1KSBw0YNF1sentjFI6wu+CRiYMpC7REf/3TQXoems2XPqIqDMlQ== | ||||||
|  | 
 | ||||||
|  | esbuild-freebsd-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.17.tgz#a3455199862110854937b05a0eecbed3e1aeec41" | ||||||
|  |   integrity sha512-2i0nTNJM8ftNTvtR00vdqkru8XpHwAbkR2MBLoK2IDSzjsLStwCj+mxf6v83eVM9Abe3QA8xP+irqOdBlwDQ2g== | ||||||
|  | 
 | ||||||
|  | esbuild-freebsd-arm64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.17.tgz#8a70f2a36f5b0da7d2efdd6fd02aa78611007fd0" | ||||||
|  |   integrity sha512-QOmRi1n+uly2G7BbMbHb86YiFA5aM7B2T96A6OF1VG57LNwXwy8LPVM0PVjl7f9cV3pE3fy3VtXPJHJo8XggTA== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-32@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.17.tgz#b7123f6e4780687e017454604d909fbe558862e9" | ||||||
|  |   integrity sha512-qG5NDk7FHHUVw01rjHESON0HvigF2X80b645TUlgTKsWRlrbzzHhMCmQguA01O5PiCimKnyoxti8aJIFNHpQnQ== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.17.tgz#47a6b510c2f7faef595a4d6257a629e65385fdc3" | ||||||
|  |   integrity sha512-De8OcmNvfNyFfQRLWbfuZqau6NpYBJxNTLP7Ls/PqQcw0HAwfaYThutY8ozHpPbKFPa7wgqabXlIC4NVSWT0/A== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-arm64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.17.tgz#dfd9022b7215ca660d464fcb20597b88887c7e64" | ||||||
|  |   integrity sha512-WDEOD/YRA4J1lxhETKZff3gRxGYqqZEiVwIOqNfvCh2YcwWU2y6UmNGZsxcuKk18wot4dAXCXQyNZgBkVUTCLw== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-arm@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.17.tgz#e6f6bb9fe52def5260d7d49b790fbec0e7c6d9cb" | ||||||
|  |   integrity sha512-ZwsgFUk3gR2pEMJdh5z4Ds18fvGETgElPqmNdx1NtZTCOVlFMAwFB5u/tOR2FrXbMFv+LkGnNxPDh48PYPDz9A== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-mips64le@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.17.tgz#bceaad33ff18a822b6da0396c6497a231397b6c3" | ||||||
|  |   integrity sha512-Lf4X9NB7r6imzp/11TaGs4kWL0DUn1JxI9gAAKotnKh6T8Y/0sLvZSvQS8WvSZcr0V8RRCrRZwiQqjOALUU/9g== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-ppc64le@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.17.tgz#9562f094d1e5e6c3b61b776b15a9bbd657042654" | ||||||
|  |   integrity sha512-aExhxbrK7/Mh9FArdiC9MbvrQz2bGCDI8cBALKJbmhKg0h7LNt6y1E1S9GGBZ/ZXkHDvV9FFVrXXZKFVU5Qpiw== | ||||||
|  | 
 | ||||||
|  | esbuild-linux-s390x@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.17.tgz#2963cfe62c227bbf1da64e36d4ca0b23db8008fe" | ||||||
|  |   integrity sha512-b0T20rNcS7POi5YLw5dFlsiC+riobR5IfppQGn5NWer6QiIkdL1vOx9eC9CUD3z1itpkLboRAZYieZfKfhCA2Q== | ||||||
|  | 
 | ||||||
|  | esbuild-netbsd-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.17.tgz#1d156023f9ae6be79b8627ab0cda2d7feb7f3a48" | ||||||
|  |   integrity sha512-pFgTaAa2JF18nqNfCND9wOu1jbZ/mbDSaMxUp5fTkLlofyHhXeb5aChgXUkeipty2Pgq0OwOnxjHmiAxMI7N4g== | ||||||
|  | 
 | ||||||
|  | esbuild-openbsd-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.17.tgz#3fc44102c9b65375385112f4ce5899ae5e38f349" | ||||||
|  |   integrity sha512-K5+plb6gsAfBcFqB0EG4KvLbgBKslVAfEyJggicwt/QoDwQGJAzao4M6zOA4PG7LlXOwWSqv7VmSFbH+b6DyKw== | ||||||
|  | 
 | ||||||
|  | esbuild-sunos-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.17.tgz#5bd24e7a7e863ea89d7e4eafd5364a155c9ea507" | ||||||
|  |   integrity sha512-o1FINkbHRi9JB1YteOSXZdkDOmVUbmnCxRmTLkHvk8pfCFNpv/5/7ktt95teYKbEiJna2dEt3M4ckJ/+UVnW+w== | ||||||
|  | 
 | ||||||
|  | esbuild-windows-32@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.17.tgz#8bda31c550fb6b425707114141d2c6ba034dab9b" | ||||||
|  |   integrity sha512-Qutilz0I7OADWBtWrC/FD+2O/TNAkhwbZ+wIns7kF87lxIMtmqpBt3KnMk1e4F47aTrZRr0oH55Zhztd7m2PAA== | ||||||
|  | 
 | ||||||
|  | esbuild-windows-64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.17.tgz#50b42c06908d3ce9fab8f0f9673199de5d0f9cbc" | ||||||
|  |   integrity sha512-b21/oRV+PHrav0HkRpKjbM2yNRVe34gAfbdMppbZFea416wa8SrjcmVfSd7n4jgqoTQG0xe+MGgOpwXtjiB3DQ== | ||||||
|  | 
 | ||||||
|  | esbuild-windows-arm64@0.14.17: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.17.tgz#62d3921a810b64a03fcace76dad4db51d2128b45" | ||||||
|  |   integrity sha512-4HN9E1idllewYvptcrrdfTA6DIWgg11kK0Zrv6yjxstJZLJeKxfilGBEaksLGs4Pst2rAYMx3H2vbYq7AWLQNA== | ||||||
|  | 
 | ||||||
|  | esbuild@^0.14.13: | ||||||
|  |   version "0.14.17" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.17.tgz#6a634e56447aa0e90b34c42091d472d802d399e5" | ||||||
|  |   integrity sha512-JLgyC6Uv31mv9T9Mm2xF1LntUMCNBSzvg2n32d8cTKZMwFr1wmMFY2FkVum98TSoEsDff0cR+Aj49H2sbBcjKQ== | ||||||
|  |   optionalDependencies: | ||||||
|  |     esbuild-android-arm64 "0.14.17" | ||||||
|  |     esbuild-darwin-64 "0.14.17" | ||||||
|  |     esbuild-darwin-arm64 "0.14.17" | ||||||
|  |     esbuild-freebsd-64 "0.14.17" | ||||||
|  |     esbuild-freebsd-arm64 "0.14.17" | ||||||
|  |     esbuild-linux-32 "0.14.17" | ||||||
|  |     esbuild-linux-64 "0.14.17" | ||||||
|  |     esbuild-linux-arm "0.14.17" | ||||||
|  |     esbuild-linux-arm64 "0.14.17" | ||||||
|  |     esbuild-linux-mips64le "0.14.17" | ||||||
|  |     esbuild-linux-ppc64le "0.14.17" | ||||||
|  |     esbuild-linux-s390x "0.14.17" | ||||||
|  |     esbuild-netbsd-64 "0.14.17" | ||||||
|  |     esbuild-openbsd-64 "0.14.17" | ||||||
|  |     esbuild-sunos-64 "0.14.17" | ||||||
|  |     esbuild-windows-32 "0.14.17" | ||||||
|  |     esbuild-windows-64 "0.14.17" | ||||||
|  |     esbuild-windows-arm64 "0.14.17" | ||||||
|  | 
 | ||||||
|  | escape-string-regexp@^4.0.0: | ||||||
|  |   version "4.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" | ||||||
|  |   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | ||||||
|  | 
 | ||||||
|  | eslint-scope@^7.1.0: | ||||||
|  |   version "7.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" | ||||||
|  |   integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== | ||||||
|  |   dependencies: | ||||||
|  |     esrecurse "^4.3.0" | ||||||
|  |     estraverse "^5.2.0" | ||||||
|  | 
 | ||||||
|  | eslint-utils@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" | ||||||
|  |   integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== | ||||||
|  |   dependencies: | ||||||
|  |     eslint-visitor-keys "^2.0.0" | ||||||
|  | 
 | ||||||
|  | eslint-visitor-keys@^2.0.0: | ||||||
|  |   version "2.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" | ||||||
|  |   integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== | ||||||
|  | 
 | ||||||
|  | eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: | ||||||
|  |   version "3.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" | ||||||
|  |   integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== | ||||||
|  | 
 | ||||||
|  | eslint@^8.2.0: | ||||||
|  |   version "8.8.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.8.0.tgz#9762b49abad0cb4952539ffdb0a046392e571a2d" | ||||||
|  |   integrity sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@eslint/eslintrc" "^1.0.5" | ||||||
|  |     "@humanwhocodes/config-array" "^0.9.2" | ||||||
|  |     ajv "^6.10.0" | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |     cross-spawn "^7.0.2" | ||||||
|  |     debug "^4.3.2" | ||||||
|  |     doctrine "^3.0.0" | ||||||
|  |     escape-string-regexp "^4.0.0" | ||||||
|  |     eslint-scope "^7.1.0" | ||||||
|  |     eslint-utils "^3.0.0" | ||||||
|  |     eslint-visitor-keys "^3.2.0" | ||||||
|  |     espree "^9.3.0" | ||||||
|  |     esquery "^1.4.0" | ||||||
|  |     esutils "^2.0.2" | ||||||
|  |     fast-deep-equal "^3.1.3" | ||||||
|  |     file-entry-cache "^6.0.1" | ||||||
|  |     functional-red-black-tree "^1.0.1" | ||||||
|  |     glob-parent "^6.0.1" | ||||||
|  |     globals "^13.6.0" | ||||||
|  |     ignore "^5.2.0" | ||||||
|  |     import-fresh "^3.0.0" | ||||||
|  |     imurmurhash "^0.1.4" | ||||||
|  |     is-glob "^4.0.0" | ||||||
|  |     js-yaml "^4.1.0" | ||||||
|  |     json-stable-stringify-without-jsonify "^1.0.1" | ||||||
|  |     levn "^0.4.1" | ||||||
|  |     lodash.merge "^4.6.2" | ||||||
|  |     minimatch "^3.0.4" | ||||||
|  |     natural-compare "^1.4.0" | ||||||
|  |     optionator "^0.9.1" | ||||||
|  |     regexpp "^3.2.0" | ||||||
|  |     strip-ansi "^6.0.1" | ||||||
|  |     strip-json-comments "^3.1.0" | ||||||
|  |     text-table "^0.2.0" | ||||||
|  |     v8-compile-cache "^2.0.3" | ||||||
|  | 
 | ||||||
|  | espree@^9.2.0, espree@^9.3.0: | ||||||
|  |   version "9.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" | ||||||
|  |   integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== | ||||||
|  |   dependencies: | ||||||
|  |     acorn "^8.7.0" | ||||||
|  |     acorn-jsx "^5.3.1" | ||||||
|  |     eslint-visitor-keys "^3.1.0" | ||||||
|  | 
 | ||||||
|  | esquery@^1.4.0: | ||||||
|  |   version "1.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" | ||||||
|  |   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== | ||||||
|  |   dependencies: | ||||||
|  |     estraverse "^5.1.0" | ||||||
|  | 
 | ||||||
|  | esrecurse@^4.3.0: | ||||||
|  |   version "4.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" | ||||||
|  |   integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== | ||||||
|  |   dependencies: | ||||||
|  |     estraverse "^5.2.0" | ||||||
|  | 
 | ||||||
|  | estraverse@^5.1.0, estraverse@^5.2.0: | ||||||
|  |   version "5.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" | ||||||
|  |   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== | ||||||
|  | 
 | ||||||
|  | esutils@^2.0.2: | ||||||
|  |   version "2.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" | ||||||
|  |   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== | ||||||
|  | 
 | ||||||
|  | eventemitter3@^4.0.7: | ||||||
|  |   version "4.0.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" | ||||||
|  |   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== | ||||||
|  | 
 | ||||||
|  | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: | ||||||
|  |   version "3.1.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" | ||||||
|  |   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== | ||||||
|  | 
 | ||||||
|  | fast-json-stable-stringify@^2.0.0: | ||||||
|  |   version "2.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" | ||||||
|  |   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== | ||||||
|  | 
 | ||||||
|  | fast-levenshtein@^2.0.6: | ||||||
|  |   version "2.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" | ||||||
|  |   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= | ||||||
|  | 
 | ||||||
|  | file-entry-cache@^6.0.1: | ||||||
|  |   version "6.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" | ||||||
|  |   integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== | ||||||
|  |   dependencies: | ||||||
|  |     flat-cache "^3.0.4" | ||||||
|  | 
 | ||||||
|  | flat-cache@^3.0.4: | ||||||
|  |   version "3.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" | ||||||
|  |   integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== | ||||||
|  |   dependencies: | ||||||
|  |     flatted "^3.1.0" | ||||||
|  |     rimraf "^3.0.2" | ||||||
|  | 
 | ||||||
|  | flatted@^3.1.0: | ||||||
|  |   version "3.2.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" | ||||||
|  |   integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== | ||||||
|  | 
 | ||||||
|  | fs.realpath@^1.0.0: | ||||||
|  |   version "1.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" | ||||||
|  |   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= | ||||||
|  | 
 | ||||||
|  | functional-red-black-tree@^1.0.1: | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" | ||||||
|  |   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= | ||||||
|  | 
 | ||||||
|  | glob-parent@^6.0.1: | ||||||
|  |   version "6.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" | ||||||
|  |   integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== | ||||||
|  |   dependencies: | ||||||
|  |     is-glob "^4.0.3" | ||||||
|  | 
 | ||||||
|  | glob@^7.1.3: | ||||||
|  |   version "7.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" | ||||||
|  |   integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== | ||||||
|  |   dependencies: | ||||||
|  |     fs.realpath "^1.0.0" | ||||||
|  |     inflight "^1.0.4" | ||||||
|  |     inherits "2" | ||||||
|  |     minimatch "^3.0.4" | ||||||
|  |     once "^1.3.0" | ||||||
|  |     path-is-absolute "^1.0.0" | ||||||
|  | 
 | ||||||
|  | globals@^13.6.0, globals@^13.9.0: | ||||||
|  |   version "13.12.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" | ||||||
|  |   integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== | ||||||
|  |   dependencies: | ||||||
|  |     type-fest "^0.20.2" | ||||||
|  | 
 | ||||||
|  | has-flag@^4.0.0: | ||||||
|  |   version "4.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" | ||||||
|  |   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== | ||||||
|  | 
 | ||||||
|  | idb-keyval@^6.0.3: | ||||||
|  |   version "6.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.1.0.tgz#e659cff41188e6097d7fadd69926f6adbbe70041" | ||||||
|  |   integrity sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw== | ||||||
|  |   dependencies: | ||||||
|  |     safari-14-idb-fix "^3.0.0" | ||||||
|  | 
 | ||||||
|  | ignore@^4.0.6: | ||||||
|  |   version "4.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" | ||||||
|  |   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== | ||||||
|  | 
 | ||||||
|  | ignore@^5.2.0: | ||||||
|  |   version "5.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" | ||||||
|  |   integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== | ||||||
|  | 
 | ||||||
|  | import-fresh@^3.0.0, import-fresh@^3.2.1: | ||||||
|  |   version "3.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" | ||||||
|  |   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== | ||||||
|  |   dependencies: | ||||||
|  |     parent-module "^1.0.0" | ||||||
|  |     resolve-from "^4.0.0" | ||||||
|  | 
 | ||||||
|  | imurmurhash@^0.1.4: | ||||||
|  |   version "0.1.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" | ||||||
|  |   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= | ||||||
|  | 
 | ||||||
|  | inflight@^1.0.4: | ||||||
|  |   version "1.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" | ||||||
|  |   integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= | ||||||
|  |   dependencies: | ||||||
|  |     once "^1.3.0" | ||||||
|  |     wrappy "1" | ||||||
|  | 
 | ||||||
|  | inherits@2: | ||||||
|  |   version "2.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" | ||||||
|  |   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== | ||||||
|  | 
 | ||||||
|  | is-extglob@^2.1.1: | ||||||
|  |   version "2.1.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" | ||||||
|  |   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= | ||||||
|  | 
 | ||||||
|  | is-glob@^4.0.0, is-glob@^4.0.3: | ||||||
|  |   version "4.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" | ||||||
|  |   integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== | ||||||
|  |   dependencies: | ||||||
|  |     is-extglob "^2.1.1" | ||||||
|  | 
 | ||||||
|  | isexe@^2.0.0: | ||||||
|  |   version "2.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" | ||||||
|  |   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= | ||||||
|  | 
 | ||||||
|  | js-yaml@^4.1.0: | ||||||
|  |   version "4.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" | ||||||
|  |   integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== | ||||||
|  |   dependencies: | ||||||
|  |     argparse "^2.0.1" | ||||||
|  | 
 | ||||||
|  | json-schema-traverse@^0.4.1: | ||||||
|  |   version "0.4.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" | ||||||
|  |   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== | ||||||
|  | 
 | ||||||
|  | json-stable-stringify-without-jsonify@^1.0.1: | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" | ||||||
|  |   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= | ||||||
|  | 
 | ||||||
|  | levn@^0.4.1: | ||||||
|  |   version "0.4.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" | ||||||
|  |   integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== | ||||||
|  |   dependencies: | ||||||
|  |     prelude-ls "^1.2.1" | ||||||
|  |     type-check "~0.4.0" | ||||||
|  | 
 | ||||||
|  | lodash.merge@^4.6.2: | ||||||
|  |   version "4.6.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" | ||||||
|  |   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== | ||||||
|  | 
 | ||||||
|  | minimatch@^3.0.4: | ||||||
|  |   version "3.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" | ||||||
|  |   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== | ||||||
|  |   dependencies: | ||||||
|  |     brace-expansion "^1.1.7" | ||||||
|  | 
 | ||||||
|  | misskey-js@0.0.14: | ||||||
|  |   version "0.0.14" | ||||||
|  |   resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" | ||||||
|  |   integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== | ||||||
|  |   dependencies: | ||||||
|  |     autobind-decorator "^2.4.0" | ||||||
|  |     eventemitter3 "^4.0.7" | ||||||
|  |     reconnecting-websocket "^4.4.0" | ||||||
|  | 
 | ||||||
|  | ms@2.1.2: | ||||||
|  |   version "2.1.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" | ||||||
|  |   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== | ||||||
|  | 
 | ||||||
|  | natural-compare@^1.4.0: | ||||||
|  |   version "1.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" | ||||||
|  |   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= | ||||||
|  | 
 | ||||||
|  | once@^1.3.0: | ||||||
|  |   version "1.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" | ||||||
|  |   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= | ||||||
|  |   dependencies: | ||||||
|  |     wrappy "1" | ||||||
|  | 
 | ||||||
|  | optionator@^0.9.1: | ||||||
|  |   version "0.9.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" | ||||||
|  |   integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== | ||||||
|  |   dependencies: | ||||||
|  |     deep-is "^0.1.3" | ||||||
|  |     fast-levenshtein "^2.0.6" | ||||||
|  |     levn "^0.4.1" | ||||||
|  |     prelude-ls "^1.2.1" | ||||||
|  |     type-check "^0.4.0" | ||||||
|  |     word-wrap "^1.2.3" | ||||||
|  | 
 | ||||||
|  | parent-module@^1.0.0: | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" | ||||||
|  |   integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== | ||||||
|  |   dependencies: | ||||||
|  |     callsites "^3.0.0" | ||||||
|  | 
 | ||||||
|  | path-is-absolute@^1.0.0: | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" | ||||||
|  |   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= | ||||||
|  | 
 | ||||||
|  | path-key@^3.1.0: | ||||||
|  |   version "3.1.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" | ||||||
|  |   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== | ||||||
|  | 
 | ||||||
|  | prelude-ls@^1.2.1: | ||||||
|  |   version "1.2.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" | ||||||
|  |   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== | ||||||
|  | 
 | ||||||
|  | punycode@^2.1.0: | ||||||
|  |   version "2.1.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" | ||||||
|  |   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== | ||||||
|  | 
 | ||||||
|  | reconnecting-websocket@^4.4.0: | ||||||
|  |   version "4.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" | ||||||
|  |   integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== | ||||||
|  | 
 | ||||||
|  | regexpp@^3.2.0: | ||||||
|  |   version "3.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" | ||||||
|  |   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== | ||||||
|  | 
 | ||||||
|  | resolve-from@^4.0.0: | ||||||
|  |   version "4.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" | ||||||
|  |   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== | ||||||
|  | 
 | ||||||
|  | rimraf@^3.0.2: | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" | ||||||
|  |   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== | ||||||
|  |   dependencies: | ||||||
|  |     glob "^7.1.3" | ||||||
|  | 
 | ||||||
|  | safari-14-idb-fix@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440" | ||||||
|  |   integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog== | ||||||
|  | 
 | ||||||
|  | shebang-command@^2.0.0: | ||||||
|  |   version "2.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" | ||||||
|  |   integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== | ||||||
|  |   dependencies: | ||||||
|  |     shebang-regex "^3.0.0" | ||||||
|  | 
 | ||||||
|  | shebang-regex@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" | ||||||
|  |   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== | ||||||
|  | 
 | ||||||
|  | strip-ansi@^6.0.1: | ||||||
|  |   version "6.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" | ||||||
|  |   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||||||
|  |   dependencies: | ||||||
|  |     ansi-regex "^5.0.1" | ||||||
|  | 
 | ||||||
|  | strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: | ||||||
|  |   version "3.1.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" | ||||||
|  |   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== | ||||||
|  | 
 | ||||||
|  | supports-color@^7.1.0: | ||||||
|  |   version "7.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" | ||||||
|  |   integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== | ||||||
|  |   dependencies: | ||||||
|  |     has-flag "^4.0.0" | ||||||
|  | 
 | ||||||
|  | text-table@^0.2.0: | ||||||
|  |   version "0.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" | ||||||
|  |   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= | ||||||
|  | 
 | ||||||
|  | type-check@^0.4.0, type-check@~0.4.0: | ||||||
|  |   version "0.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" | ||||||
|  |   integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== | ||||||
|  |   dependencies: | ||||||
|  |     prelude-ls "^1.2.1" | ||||||
|  | 
 | ||||||
|  | type-fest@^0.20.2: | ||||||
|  |   version "0.20.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" | ||||||
|  |   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== | ||||||
|  | 
 | ||||||
|  | uri-js@^4.2.2: | ||||||
|  |   version "4.4.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" | ||||||
|  |   integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== | ||||||
|  |   dependencies: | ||||||
|  |     punycode "^2.1.0" | ||||||
|  | 
 | ||||||
|  | v8-compile-cache@^2.0.3: | ||||||
|  |   version "2.3.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | ||||||
|  |   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== | ||||||
|  | 
 | ||||||
|  | which@^2.0.1: | ||||||
|  |   version "2.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" | ||||||
|  |   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== | ||||||
|  |   dependencies: | ||||||
|  |     isexe "^2.0.0" | ||||||
|  | 
 | ||||||
|  | word-wrap@^1.2.3: | ||||||
|  |   version "1.2.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | ||||||
|  |   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== | ||||||
|  | 
 | ||||||
|  | wrappy@1: | ||||||
|  |   version "1.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" | ||||||
|  |   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= | ||||||
|  | @ -17,6 +17,14 @@ const execa = require('execa'); | ||||||
| 		stderr: process.stderr, | 		stderr: process.stderr, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	console.log('building packages/sw ...'); | ||||||
|  | 
 | ||||||
|  | 	await execa('npm', ['run', 'build'], { | ||||||
|  | 		cwd: __dirname + '/../packages/sw', | ||||||
|  | 		stdout: process.stdout, | ||||||
|  | 		stderr: process.stderr, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	console.log('build finishing ...'); | 	console.log('build finishing ...'); | ||||||
| 
 | 
 | ||||||
| 	await execa('npm', ['run', 'gulp'], { | 	await execa('npm', ['run', 'gulp'], { | ||||||
|  |  | ||||||
|  | @ -7,6 +7,9 @@ const fs = require('fs'); | ||||||
| 	fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); | ||||||
| 	fs.rmSync(__dirname + '/../packages/client/node_modules', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../packages/client/node_modules', { recursive: true, force: true }); | ||||||
| 
 | 
 | ||||||
|  | 	fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); | ||||||
|  | 	fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true }); | ||||||
|  | 
 | ||||||
| 	fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); | ||||||
| 	fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true }); | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -3,5 +3,6 @@ const fs = require('fs'); | ||||||
| (async () => { | (async () => { | ||||||
| 	fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true }); | ||||||
| 	fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); | ||||||
|  | 	fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); | ||||||
| 	fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); | 	fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -25,6 +25,12 @@ const execa = require('execa'); | ||||||
| 		stderr: process.stderr, | 		stderr: process.stderr, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	execa('npm', ['run', 'watch'], { | ||||||
|  | 		cwd: __dirname + '/../packages/sw', | ||||||
|  | 		stdout: process.stdout, | ||||||
|  | 		stderr: process.stderr, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	const start = async () => { | 	const start = async () => { | ||||||
| 		try { | 		try { | ||||||
| 			await execa('npm', ['run', 'start'], { | 			await execa('npm', ['run', 'start'], { | ||||||
|  |  | ||||||
|  | @ -16,4 +16,12 @@ const execa = require('execa'); | ||||||
| 		stdout: process.stdout, | 		stdout: process.stdout, | ||||||
| 		stderr: process.stderr, | 		stderr: process.stderr, | ||||||
| 	}); | 	}); | ||||||
|  | 
 | ||||||
|  | 	console.log('installing dependencies of packages/sw ...'); | ||||||
|  | 
 | ||||||
|  | 	await execa('yarn', ['install'], { | ||||||
|  | 		cwd: __dirname + '/../packages/sw', | ||||||
|  | 		stdout: process.stdout, | ||||||
|  | 		stderr: process.stderr, | ||||||
|  | 	}); | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -14,4 +14,11 @@ const execa = require('execa'); | ||||||
| 		stdout: process.stdout, | 		stdout: process.stdout, | ||||||
| 		stderr: process.stderr, | 		stderr: process.stderr, | ||||||
| 	}); | 	}); | ||||||
|  | 
 | ||||||
|  | 	console.log('linting packages/sw ...'); | ||||||
|  | 	await execa('npm', ['run', 'lint'], { | ||||||
|  | 		cwd: __dirname + '/../packages/sw', | ||||||
|  | 		stdout: process.stdout, | ||||||
|  | 		stderr: process.stderr, | ||||||
|  | 	}); | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue