From c05cd37c3caea091b8850c7cd78187392e5dd996 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 26 Mar 2022 23:30:44 +0800 Subject: [PATCH 1/4] refactor(types): share hot context type --- packages/vite/src/client/client.ts | 16 +++++------ packages/vite/types/hot.d.ts | 43 +++++++++++++++++++++++++++++ packages/vite/types/importMeta.d.ts | 43 +---------------------------- 3 files changed, 52 insertions(+), 50 deletions(-) create mode 100644 packages/vite/types/hot.d.ts diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 40f0bb0418f365..1fe461f5b04caf 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -6,6 +6,7 @@ import type { Update, UpdatePayload } from 'types/hmrPayload' +import type { ViteHotContext } from 'types/hot' import type { CustomEventName } from 'types/customEvent' import { ErrorOverlay, overlayId } from './overlay' // eslint-disable-next-line node/no-missing-import @@ -391,9 +392,7 @@ const ctxToListenersMap = new Map< Map void)[]> >() -// Just infer the return type for now -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const createHotContext = (ownerPath: string) => { +export function createHotContext(ownerPath: string): ViteHotContext { if (!dataMap.has(ownerPath)) { dataMap.set(ownerPath, {}) } @@ -434,12 +433,12 @@ export const createHotContext = (ownerPath: string) => { hotModulesMap.set(ownerPath, mod) } - const hot = { + const hot: ViteHotContext = { get data() { return dataMap.get(ownerPath) }, - accept(deps: any, callback?: any) { + accept(deps?: any, callback?: any) { if (typeof deps === 'function' || !deps) { // self-accept: hot.accept(() => {}) acceptDeps([ownerPath], ([mod]) => deps && deps(mod)) @@ -460,10 +459,11 @@ export const createHotContext = (ownerPath: string) => { ) }, - dispose(cb: (data: any) => void) { + dispose(cb) { disposeMap.set(ownerPath, cb) }, + // @ts-expect-error untyped prune(cb: (data: any) => void) { pruneMap.set(ownerPath, cb) }, @@ -479,7 +479,7 @@ export const createHotContext = (ownerPath: string) => { }, // custom events - on: (event: string, cb: (data: any) => void) => { + on(event, cb) { const addToMap = (map: Map) => { const existing = map.get(event) || [] existing.push(cb) @@ -489,7 +489,7 @@ export const createHotContext = (ownerPath: string) => { addToMap(newListeners) }, - send: (event: string, data?: any) => { + send(event, data) { messageBuffer.push(JSON.stringify({ type: 'custom', event, data })) sendMessageBuffer() } diff --git a/packages/vite/types/hot.d.ts b/packages/vite/types/hot.d.ts new file mode 100644 index 00000000000000..ee7c660056d086 --- /dev/null +++ b/packages/vite/types/hot.d.ts @@ -0,0 +1,43 @@ +import type { + ErrorPayload, + FullReloadPayload, + PrunePayload, + UpdatePayload +} from './hmrPayload' + +export interface ViteHotContext { + readonly data: any + + accept(): void + accept(cb: (mod: any) => void): void + accept(dep: string, cb: (mod: any) => void): void + accept(deps: readonly string[], cb: (mods: any[]) => void): void + + /** + * @deprecated + */ + acceptDeps(): never + + dispose(cb: (data: any) => void): void + decline(): void + invalidate(): void + + on: { + (event: 'vite:beforeUpdate', cb: (payload: UpdatePayload) => void): void + (event: 'vite:beforePrune', cb: (payload: PrunePayload) => void): void + ( + event: 'vite:beforeFullReload', + cb: (payload: FullReloadPayload) => void + ): void + (event: 'vite:error', cb: (payload: ErrorPayload) => void): void + (event: string, cb: (data: any) => void): void + } + + send(event: string, data?: any): void +} + +// See https://stackoverflow.com/a/63549561. +export type CustomEventName = (T extends `vite:${T}` + ? never + : T) & + (`vite:${T}` extends T ? never : T) diff --git a/packages/vite/types/importMeta.d.ts b/packages/vite/types/importMeta.d.ts index 1fd39b993d5142..9b57fd120a7ba9 100644 --- a/packages/vite/types/importMeta.d.ts +++ b/packages/vite/types/importMeta.d.ts @@ -20,48 +20,7 @@ interface GlobOptions { interface ImportMeta { url: string - readonly hot?: { - readonly data: any - - accept(): void - accept(cb: (mod: any) => void): void - accept(dep: string, cb: (mod: any) => void): void - accept(deps: readonly string[], cb: (mods: any[]) => void): void - - /** - * @deprecated - */ - acceptDeps(): never - - dispose(cb: (data: any) => void): void - decline(): void - invalidate(): void - - on: { - ( - event: 'vite:beforeUpdate', - cb: (payload: import('./hmrPayload').UpdatePayload) => void - ): void - ( - event: 'vite:beforePrune', - cb: (payload: import('./hmrPayload').PrunePayload) => void - ): void - ( - event: 'vite:beforeFullReload', - cb: (payload: import('./hmrPayload').FullReloadPayload) => void - ): void - ( - event: 'vite:error', - cb: (payload: import('./hmrPayload').ErrorPayload) => void - ): void - ( - event: import('./customEvent').CustomEventName, - cb: (data: any) => void - ): void - } - - send(event: string, data?: any): void - } + readonly hot?: import('./hot').ViteHotContext readonly env: ImportMetaEnv From 1ed8bc11a4f7381c0e2efc1f058aae227434a79b Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 26 Mar 2022 23:52:29 +0800 Subject: [PATCH 2/4] feat(type): support typing for custom events --- docs/guide/api-plugin.md | 16 ++++++++++ packages/playground/hmr/event.d.ts | 9 ++++++ packages/playground/hmr/{hmr.js => hmr.ts} | 8 ++--- packages/playground/hmr/index.html | 2 +- packages/playground/hmr/tsconfig.json | 15 ++++++++++ .../hmr/{vite.config.js => vite.config.ts} | 15 +++++----- packages/vite/src/client/client.ts | 27 ++++------------- packages/vite/src/node/server/ws.ts | 11 +++++-- packages/vite/types/customEvent.d.ts | 21 +++++++++---- packages/vite/types/hot.d.ts | 30 ++++--------------- 10 files changed, 87 insertions(+), 67 deletions(-) create mode 100644 packages/playground/hmr/event.d.ts rename packages/playground/hmr/{hmr.js => hmr.ts} (87%) create mode 100644 packages/playground/hmr/tsconfig.json rename packages/playground/hmr/{vite.config.js => vite.config.ts} (56%) diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index 13767c45dd3103..228755dd6f85c2 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -560,3 +560,19 @@ export default defineConfig({ ] }) ``` + +### TypeScript for Custom Events + +It is possible to type custom events by extending the `CustomEventMap` interface: + +```ts +// events.d.ts +import 'vite/types/customEvent' + +declare module 'vite/types/customEvent' { + interface CustomEventMap { + 'custom:foo': { msg: string } + // 'event-key': payload + } +} +``` diff --git a/packages/playground/hmr/event.d.ts b/packages/playground/hmr/event.d.ts new file mode 100644 index 00000000000000..151a9cc3b861cd --- /dev/null +++ b/packages/playground/hmr/event.d.ts @@ -0,0 +1,9 @@ +import 'vite/types/customEvent' + +declare module 'vite/types/customEvent' { + interface CustomEventMap { + 'custom:foo': { msg: string } + 'custom:remote-add': { a: number; b: number } + 'custom:remote-add-result': { result: string } + } +} diff --git a/packages/playground/hmr/hmr.js b/packages/playground/hmr/hmr.ts similarity index 87% rename from packages/playground/hmr/hmr.js rename to packages/playground/hmr/hmr.ts index e80b517e6449dc..113b87bc5865d4 100644 --- a/packages/playground/hmr/hmr.js +++ b/packages/playground/hmr/hmr.ts @@ -41,7 +41,7 @@ if (import.meta.hot) { update.type === 'css-update' && update.path.match('global.css') ) if (cssUpdate) { - const el = document.querySelector('#global-css') + const el = document.querySelector('#global-css') as HTMLLinkElement text('.css-prev', el.href) // We don't have a vite:afterUpdate event, but updates are currently sync setTimeout(() => { @@ -54,13 +54,13 @@ if (import.meta.hot) { console.log(`>>> vite:error -- ${event.type}`) }) - import.meta.hot.on('foo', ({ msg }) => { + import.meta.hot.on('custom:foo', ({ msg }) => { text('.custom', msg) }) // send custom event to server to calculate 1 + 2 - import.meta.hot.send('remote-add', { a: 1, b: 2 }) - import.meta.hot.on('remote-add-result', ({ result }) => { + import.meta.hot.send('custom:remote-add', { a: 1, b: 2 }) + import.meta.hot.on('custom:remote-add-result', ({ result }) => { text('.custom-communication', result) }) } diff --git a/packages/playground/hmr/index.html b/packages/playground/hmr/index.html index fc398c60c4cadf..0add7c26011a01 100644 --- a/packages/playground/hmr/index.html +++ b/packages/playground/hmr/index.html @@ -1,5 +1,5 @@ - +
diff --git a/packages/playground/hmr/tsconfig.json b/packages/playground/hmr/tsconfig.json new file mode 100644 index 00000000000000..41b16fdc65ec8c --- /dev/null +++ b/packages/playground/hmr/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["."], + "exclude": ["**/dist/**"], + "compilerOptions": { + "target": "es2019", + "module": "esnext", + "outDir": "dist", + "allowJs": true, + "esModuleInterop": true, + "moduleResolution": "node", + "baseUrl": ".", + "jsx": "preserve", + "types": ["vite/client", "jest", "node"] + } +} diff --git a/packages/playground/hmr/vite.config.js b/packages/playground/hmr/vite.config.ts similarity index 56% rename from packages/playground/hmr/vite.config.js rename to packages/playground/hmr/vite.config.ts index 57252c91be410b..ef5d3cf36a2fcb 100644 --- a/packages/playground/hmr/vite.config.js +++ b/packages/playground/hmr/vite.config.ts @@ -1,7 +1,6 @@ -/** - * @type {import('vite').UserConfig} - */ -module.exports = { +import { defineConfig } from 'vite' + +export default defineConfig({ plugins: [ { name: 'mock-custom', @@ -9,14 +8,14 @@ module.exports = { if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.ws.send('foo', { msg }) + server.ws.send('custom:foo', { msg }) } }, configureServer(server) { - server.ws.on('remote-add', ({ a, b }, client) => { - client.send('remote-add-result', { result: a + b }) + server.ws.on('custom:remote-add', ({ a, b }, client) => { + client.send('custom:remote-add-result', { result: a + b }) }) } } ] -} +}) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 1fe461f5b04caf..77bc8109b8c7e4 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,13 +1,6 @@ -import type { - ErrorPayload, - FullReloadPayload, - HMRPayload, - PrunePayload, - Update, - UpdatePayload -} from 'types/hmrPayload' +import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload' import type { ViteHotContext } from 'types/hot' -import type { CustomEventName } from 'types/customEvent' +import type { GetCustomEventPayload } from 'types/customEvent' import { ErrorOverlay, overlayId } from './overlay' // eslint-disable-next-line node/no-missing-import import '@vite/env' @@ -104,7 +97,7 @@ async function handleMessage(payload: HMRPayload) { }) break case 'custom': { - notifyListeners(payload.event as CustomEventName, payload.data) + notifyListeners(payload.event, payload.data) break } case 'full-reload': @@ -157,19 +150,9 @@ async function handleMessage(payload: HMRPayload) { } } -function notifyListeners( - event: 'vite:beforeUpdate', - payload: UpdatePayload -): void -function notifyListeners(event: 'vite:beforePrune', payload: PrunePayload): void -function notifyListeners( - event: 'vite:beforeFullReload', - payload: FullReloadPayload -): void -function notifyListeners(event: 'vite:error', payload: ErrorPayload): void function notifyListeners( - event: CustomEventName, - data: any + event: T, + data: GetCustomEventPayload ): void function notifyListeners(event: string, data: any): void { const cbs = customListenersMap.get(event) diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 3c6875e2475c64..7ff0eaf96dec95 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -6,9 +6,11 @@ import { createServer as createHttpsServer } from 'https' import type { ServerOptions, WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw } from 'ws' import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { GetCustomEventPayload } from 'types/customEvent' import type { ResolvedConfig } from '..' import { isObject } from '../utils' import type { Socket } from 'net' + export const HMR_HEADER = 'vite-hmr' export type WebSocketCustomListener = ( @@ -28,7 +30,7 @@ export interface WebSocketServer { /** * Send custom event */ - send(event: string, payload?: CustomPayload['data']): void + send(event: T, payload?: GetCustomEventPayload): void /** * Disconnect all clients and terminate the server. */ @@ -37,13 +39,16 @@ export interface WebSocketServer { * Handle custom event emitted by `import.meta.hot.send` */ on: WebSocketServerRaw['on'] & { - (event: string, listener: WebSocketCustomListener): void + ( + event: T, + listener: WebSocketCustomListener> + ): void } /** * Unregister event listener. */ off: WebSocketServerRaw['off'] & { - (event: string, listener: WebSocketCustomListener): void + (event: string, listener: Function): void } } diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index c38a4ac940ff6e..a12a3c9fb671b0 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -1,5 +1,16 @@ -// See https://stackoverflow.com/a/63549561. -export type CustomEventName = (T extends `vite:${T}` - ? never - : T) & - (`vite:${T}` extends T ? never : T) +import type { + ErrorPayload, + FullReloadPayload, + PrunePayload, + UpdatePayload +} from './hmrPayload' + +export interface CustomEventMap { + 'vite:beforeUpdate': UpdatePayload + 'vite:beforePrune': PrunePayload + 'vite:beforeFullReload': FullReloadPayload + 'vite:error': ErrorPayload +} + +export type GetCustomEventPayload = + T extends keyof CustomEventMap ? CustomEventMap[T] : unknown diff --git a/packages/vite/types/hot.d.ts b/packages/vite/types/hot.d.ts index ee7c660056d086..9810b370f03141 100644 --- a/packages/vite/types/hot.d.ts +++ b/packages/vite/types/hot.d.ts @@ -1,9 +1,4 @@ -import type { - ErrorPayload, - FullReloadPayload, - PrunePayload, - UpdatePayload -} from './hmrPayload' +import type { GetCustomEventPayload } from './customEvent' export interface ViteHotContext { readonly data: any @@ -22,22 +17,9 @@ export interface ViteHotContext { decline(): void invalidate(): void - on: { - (event: 'vite:beforeUpdate', cb: (payload: UpdatePayload) => void): void - (event: 'vite:beforePrune', cb: (payload: PrunePayload) => void): void - ( - event: 'vite:beforeFullReload', - cb: (payload: FullReloadPayload) => void - ): void - (event: 'vite:error', cb: (payload: ErrorPayload) => void): void - (event: string, cb: (data: any) => void): void - } - - send(event: string, data?: any): void + on( + event: T, + cb: (payload: GetCustomEventPayload) => void + ): void + send(event: T, data?: GetCustomEventPayload): void } - -// See https://stackoverflow.com/a/63549561. -export type CustomEventName = (T extends `vite:${T}` - ? never - : T) & - (`vite:${T}` extends T ? never : T) From ab785d34d5ab617d4acc2d95b9e637ed5f961e35 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 26 Mar 2022 23:57:45 +0800 Subject: [PATCH 3/4] chore: update --- packages/vite/src/client/client.ts | 4 ++-- packages/vite/src/node/index.ts | 1 + packages/vite/src/node/server/ws.ts | 6 +++--- packages/vite/types/customEvent.d.ts | 2 +- packages/vite/types/hot.d.ts | 6 +++--- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 77bc8109b8c7e4..c180714f5a69bf 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,6 +1,6 @@ import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload' import type { ViteHotContext } from 'types/hot' -import type { GetCustomEventPayload } from 'types/customEvent' +import type { InferCustomEventPayload } from 'types/customEvent' import { ErrorOverlay, overlayId } from './overlay' // eslint-disable-next-line node/no-missing-import import '@vite/env' @@ -152,7 +152,7 @@ async function handleMessage(payload: HMRPayload) { function notifyListeners( event: T, - data: GetCustomEventPayload + data: InferCustomEventPayload ): void function notifyListeners(event: string, data: any): void { const cbs = customListenersMap.get(event) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 2b59da8737029b..2e849d846527ca 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -108,6 +108,7 @@ export type { export type { Terser } from 'types/terser' export type { RollupCommonJSOptions } from 'types/commonjs' export type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars' +export type { CustomEventMap, InferCustomEventPayload } from 'types/customEvent' export type { Matcher, AnymatchPattern, AnymatchFn } from 'types/anymatch' export type { SplitVendorChunkCache } from './plugins/splitVendorChunk' diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 7ff0eaf96dec95..a01d0bd3225571 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -6,7 +6,7 @@ import { createServer as createHttpsServer } from 'https' import type { ServerOptions, WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw } from 'ws' import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload' -import type { GetCustomEventPayload } from 'types/customEvent' +import type { InferCustomEventPayload } from 'types/customEvent' import type { ResolvedConfig } from '..' import { isObject } from '../utils' import type { Socket } from 'net' @@ -30,7 +30,7 @@ export interface WebSocketServer { /** * Send custom event */ - send(event: T, payload?: GetCustomEventPayload): void + send(event: T, payload?: InferCustomEventPayload): void /** * Disconnect all clients and terminate the server. */ @@ -41,7 +41,7 @@ export interface WebSocketServer { on: WebSocketServerRaw['on'] & { ( event: T, - listener: WebSocketCustomListener> + listener: WebSocketCustomListener> ): void } /** diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index a12a3c9fb671b0..aacf1554b0de75 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -12,5 +12,5 @@ export interface CustomEventMap { 'vite:error': ErrorPayload } -export type GetCustomEventPayload = +export type InferCustomEventPayload = T extends keyof CustomEventMap ? CustomEventMap[T] : unknown diff --git a/packages/vite/types/hot.d.ts b/packages/vite/types/hot.d.ts index 9810b370f03141..f06846ff59d530 100644 --- a/packages/vite/types/hot.d.ts +++ b/packages/vite/types/hot.d.ts @@ -1,4 +1,4 @@ -import type { GetCustomEventPayload } from './customEvent' +import type { InferCustomEventPayload } from './customEvent' export interface ViteHotContext { readonly data: any @@ -19,7 +19,7 @@ export interface ViteHotContext { on( event: T, - cb: (payload: GetCustomEventPayload) => void + cb: (payload: InferCustomEventPayload) => void ): void - send(event: T, data?: GetCustomEventPayload): void + send(event: T, data?: InferCustomEventPayload): void } From 08a48e69faa199a7bc0ae84ea239ce4bdaa64faf Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 27 Mar 2022 00:12:28 +0800 Subject: [PATCH 4/4] chore: fix --- packages/playground/hmr/__tests__/hmr.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/playground/hmr/__tests__/hmr.spec.ts b/packages/playground/hmr/__tests__/hmr.spec.ts index 4d0491af91a69e..6ddc2345ae4fb4 100644 --- a/packages/playground/hmr/__tests__/hmr.spec.ts +++ b/packages/playground/hmr/__tests__/hmr.spec.ts @@ -16,7 +16,7 @@ if (!isBuild) { test('self accept', async () => { const el = await page.$('.app') - editFile('hmr.js', (code) => code.replace('const foo = 1', 'const foo = 2')) + editFile('hmr.ts', (code) => code.replace('const foo = 1', 'const foo = 2')) await untilUpdated(() => el.textContent(), '2') expect(browserLogs).toMatchObject([ @@ -24,11 +24,11 @@ if (!isBuild) { 'foo was: 1', '(self-accepting 1) foo is now: 2', '(self-accepting 2) foo is now: 2', - '[vite] hot updated: /hmr.js' + '[vite] hot updated: /hmr.ts' ]) browserLogs.length = 0 - editFile('hmr.js', (code) => code.replace('const foo = 2', 'const foo = 3')) + editFile('hmr.ts', (code) => code.replace('const foo = 2', 'const foo = 3')) await untilUpdated(() => el.textContent(), '3') expect(browserLogs).toMatchObject([ @@ -36,7 +36,7 @@ if (!isBuild) { 'foo was: 2', '(self-accepting 1) foo is now: 3', '(self-accepting 2) foo is now: 3', - '[vite] hot updated: /hmr.js' + '[vite] hot updated: /hmr.ts' ]) browserLogs.length = 0 }) @@ -57,7 +57,7 @@ if (!isBuild) { '(single dep) nested foo is now: 1', '(multi deps) foo is now: 2', '(multi deps) nested foo is now: 1', - '[vite] hot updated: /hmrDep.js via /hmr.js' + '[vite] hot updated: /hmrDep.js via /hmr.ts' ]) browserLogs.length = 0 @@ -74,7 +74,7 @@ if (!isBuild) { '(single dep) nested foo is now: 1', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 1', - '[vite] hot updated: /hmrDep.js via /hmr.js' + '[vite] hot updated: /hmrDep.js via /hmr.ts' ]) browserLogs.length = 0 }) @@ -95,7 +95,7 @@ if (!isBuild) { '(single dep) nested foo is now: 2', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 2', - '[vite] hot updated: /hmrDep.js via /hmr.js' + '[vite] hot updated: /hmrDep.js via /hmr.ts' ]) browserLogs.length = 0 @@ -112,7 +112,7 @@ if (!isBuild) { '(single dep) nested foo is now: 3', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 3', - '[vite] hot updated: /hmrDep.js via /hmr.js' + '[vite] hot updated: /hmrDep.js via /hmr.ts' ]) browserLogs.length = 0 })