Skip to content

Commit

Permalink
feat: resume overlay when reload page
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Feb 6, 2022
1 parent 9681f32 commit 7939381
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 54 deletions.
46 changes: 35 additions & 11 deletions packages/vite-plugin-checker/src/@runtime/main.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,63 @@
import type { ErrorPayload } from 'vite'
import { ErrorOverlay, overlayId } from './overlay'
import { ErrorOverlay, overlayId, OverlayPayload, OverlayData, WsReconnectPayload } from './overlay'
let enableOverlay = true

const WS_CHECKER_ERROR_TYPE = 'vite-plugin-checker-error'
const WS_CHECKER_ERROR_EVENT = 'vite-plugin-checker:error'
const WS_CHECKER_RECONNECT_EVENT = 'vite-plugin-checker:reconnect'

export function inject() {
const socketProtocol = null || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${null || location.hostname}:${'3000'}`
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')

socket.addEventListener('message', async ({ data: dataStr }) => {
const data = JSON.parse(dataStr)
const data: OverlayPayload = JSON.parse(dataStr)
switch (data.type) {
case 'update':
clearErrorOverlay()
// clearErrorOverlay()
break
case 'full-reload':
break
default:
break
}

if (data.event === WS_CHECKER_ERROR_TYPE) {
if (data.data.errors.length > 0) {
createErrorOverlay(data.data.errors)
if (data.type === 'custom') {
if (data.event === WS_CHECKER_ERROR_EVENT) {
updateErrorOverlay(data.data)
}

if (data.event === WS_CHECKER_RECONNECT_EVENT) {
resumeErrorOverlay(data.data)
}
}
})
}

function createErrorOverlay(err: ErrorPayload['err'][]) {
let overlayEle: ErrorOverlay | null = null

function resumeErrorOverlay(data: WsReconnectPayload['data']) {
const payloadsToResume = data.map((d) => d.data)
if (payloadsToResume.some((p) => p.errors.length)) {
overlayEle = new ErrorOverlay(data.map((d) => d.data))
document.body.appendChild(overlayEle)
}
}

function updateErrorOverlay(err: OverlayData) {
if (!enableOverlay) return
clearErrorOverlay()
document.body.appendChild(new ErrorOverlay(err))

if (overlayEle) {
overlayEle.updateErrors(err)
if (!overlayEle.getErrorCount()) {
clearErrorOverlay()
}
} else {
overlayEle = new ErrorOverlay(err)
document.body.appendChild(overlayEle)
}
}

function clearErrorOverlay() {
document.querySelectorAll(overlayId).forEach((n) => (n as ErrorOverlay).close())
overlayEle = null
}
90 changes: 74 additions & 16 deletions packages/vite-plugin-checker/src/@runtime/overlay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import type { ErrorPayload } from 'vite'

export type OverlayPayload =
| {
type: 'full-reload' | 'update'
}
| (WsErrorPayload | WsReconnectPayload)

export interface WsErrorPayload {
type: 'custom'
event: 'vite-plugin-checker:error'
data: OverlayData
}

export interface WsReconnectPayload {
type: 'custom'
event: 'vite-plugin-checker:reconnect'
data: WsErrorPayload[]
}

export interface OverlayData {
checkerId: string
errors: ErrorPayload['err'][]
}

const template = (errors: string[]) => `
<style>
:host {
Expand Down Expand Up @@ -91,7 +114,7 @@ pre::-webkit-scrollbar {
}
.tip {
font-size: 13px;
font-size: 12px;
color: #999;
border-top: 1px dotted #999;
padding-top: 13px;
Expand All @@ -109,11 +132,12 @@ code {
}
</style>
<div class="window">
${errors.join('\n')}
<div class="message-list">
${errors.join('\n')}
</div>
<div class="tip">
Click outside or fix the code to dismiss.<br>
You can also disable this overlay by setting
<code>server.hmr.overlay</code> to <code>false</code> in <code>vite.config.js.</code>
Click outside or fix the code to dismiss. You can also disable this overlay by setting
<code>config.overlay</code> to <code>false</code> in <code>vite.config.js.</code>
</div>
</div>
`
Expand All @@ -124,7 +148,6 @@ const errorTemplate = `
<pre class="file"></pre>
<pre class="frame"></pre>
<pre class="stack"></pre>
</div>
`

Expand All @@ -134,19 +157,52 @@ const codeframeRE = /^(?:>?\s+\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm
export class ErrorOverlay extends HTMLElement {
public root: ShadowRoot

public constructor(errs: ErrorPayload['err'][]) {
public constructor(data: OverlayData | OverlayData[]) {
super()

this.root = this.attachShadow({ mode: 'open' })
this.root.innerHTML = template(new Array(errs.length).fill(errorTemplate))
this.prepareWindow()
// this.root.innerHTML = template(new Array(errors.length).fill(errorTemplate))

if (Array.isArray(data)) {
if (data.length) {
data.forEach((item) => this.appendErrors(item))
} else {
return
}
} else {
this.appendErrors(data)
}

this.root.querySelector('.window')!.addEventListener('click', (e) => {
e.stopPropagation()
})

this.addEventListener('click', () => {
this.close()
})
}

public clearErrors(checkerId: string) {
this.root.querySelectorAll(`.message-list .${checkerId}`).forEach((el) => el.remove())
}

public prepareWindow() {
this.root.innerHTML = template([])
}

errs.forEach((err, index) => {
public appendErrors({ errors, checkerId }: OverlayData) {
const toAppend = new Array(errors.length).fill(errorTemplate)
this.root.querySelector('.message-list')!.innerHTML += toAppend
errors.forEach((err, index) => {
codeframeRE.lastIndex = 0
const hasFrame = err.frame && codeframeRE.test(err.frame)
const message = hasFrame ? err.message.replace(codeframeRE, '') : err.message
const selectorPrefix = `.message-item:nth-child(${index + 1}) `

this.root.querySelectorAll(selectorPrefix).forEach((el) => el.classList.add(checkerId))
if (err.plugin) {
this.text(selectorPrefix + '.plugin', `[plugin:${err.plugin}] `)
this.text(selectorPrefix + '.plugin', `[${err.plugin}] `)
}
this.text(selectorPrefix + '.message-body', message.trim())

Expand All @@ -162,13 +218,15 @@ export class ErrorOverlay extends HTMLElement {
}
this.text(selectorPrefix + '.stack', err.stack, true)
})
}

this.root.querySelector('.window')!.addEventListener('click', (e) => {
e.stopPropagation()
})
this.addEventListener('click', () => {
this.close()
})
public updateErrors({ errors, checkerId }: OverlayData) {
this.clearErrors(checkerId)
this.appendErrors({ errors, checkerId })
}

public getErrorCount() {
return this.root.querySelectorAll('.message-item').length
}

public text(selector: string, text: string, linkFiles = false): void {
Expand Down
5 changes: 3 additions & 2 deletions packages/vite-plugin-checker/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import fs from 'fs'

const RUNTIME_PUBLIC_PATH = '/@vite-plugin-checker-runtime'
const RUNTIME_FILE_PATH = require.resolve('../@runtime/main.js')
const WS_CHECKER_ERROR_TYPE = 'vite-plugin-checker-error'
const WS_CHECKER_ERROR_EVENT = 'vite-plugin-checker:error'
const WS_CHECKER_RECONNECT_EVENT = 'vite-plugin-checker:reconnect'

const runtimeCode = `${fs.readFileSync(RUNTIME_FILE_PATH, 'utf-8')};`

export { runtimeCode, RUNTIME_PUBLIC_PATH, WS_CHECKER_ERROR_TYPE }
export { runtimeCode, RUNTIME_PUBLIC_PATH, WS_CHECKER_ERROR_EVENT, WS_CHECKER_RECONNECT_EVENT }
6 changes: 3 additions & 3 deletions packages/vite-plugin-checker/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isMainThread, parentPort } from 'worker_threads'

import { codeFrameColumns, SourceLocation } from '@babel/code-frame'

import { WS_CHECKER_ERROR_TYPE } from './client/index'
import { WS_CHECKER_ERROR_EVENT } from './client/index'
import { ACTION_TYPES } from './types'

import type { Range } from 'vscode-languageclient'
Expand Down Expand Up @@ -105,7 +105,7 @@ export function diagnosticToViteError(
typeof d.stack === 'string' ? d.stack : Array.isArray(d.stack) ? d.stack.join(os.EOL) : '',
id: d.id,
frame: d.stripedCodeFrame,
plugin: `vite-plugin-checker(${d.checker})`,
plugin: d.checker,
loc,
}
})
Expand All @@ -116,7 +116,7 @@ export function diagnosticToViteError(
export function toViteCustomPayload(id: string, errors: ErrorPayload['err'][]): CustomPayload {
return {
type: 'custom',
event: WS_CHECKER_ERROR_TYPE,
event: WS_CHECKER_ERROR_EVENT,
data: {
checkerId: id,
errors,
Expand Down
21 changes: 12 additions & 9 deletions packages/vite-plugin-checker/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import npmRunPath from 'npm-run-path'
import { ConfigEnv, Plugin } from 'vite'

import { Checker } from './Checker'
import { RUNTIME_PUBLIC_PATH, runtimeCode, WS_CHECKER_ERROR_TYPE } from './client/index'
import { RUNTIME_PUBLIC_PATH, runtimeCode, WS_CHECKER_RECONNECT_EVENT } from './client/index'
import {
ACTION_TYPES,
BuildCheckBinStr,
Expand Down Expand Up @@ -139,14 +139,17 @@ export default function Plugin(userConfig: UserPluginConfig): Plugin {
// sometimes Vite will trigger a full-reload instead of HMR, but the checker
// may update the overlay before full-reload fired. So we make sure the overlay
// will be displayed again after full-reload.
// server.ws.on('connection', () => {
// connectedTimes++
// // if connectedCount !== 1, means Vite is doing a full-reload, so we don't need to send overlay again
// const latestOverlayError = latestOverlayErrors.filter(Boolean).slice(-1)[0]
// if (connectedTimes > 1 && latestOverlayError) {
// server.ws.send(latestOverlayError)
// }
// })
server.ws.on('connection', () => {
connectedTimes++
// if connectedCount !== 1, means Vite is doing a full-reload, so we don't need to send overlay again
if (connectedTimes > 1) {
server.ws.send({
type: 'custom',
event: WS_CHECKER_RECONNECT_EVENT,
data: latestOverlayErrors.filter(Boolean),
})
}
})

server.middlewares.use((req, res, next) => {
next()
Expand Down
4 changes: 2 additions & 2 deletions playground/react-ts/__tests__/__snapshots__/test.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`typescript serve get initial error and subsequent error 1`] = `"[{\\"frame\\":\\" 4 |/n 5 | function App() {/n > 6 | const [count, setCount] = useState<string>(1)/n | ^/n 7 | return (/n 8 | <div className=/\\"App/\\">/n 9 | <header className=/\\"App-header/\\">\\",\\"id\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"loc\\":{\\"column\\":46,\\"file\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"line\\":6},\\"message\\":\\"Argument of type 'number' is not assignable to parameter of type 'string | (() => string)'.\\",\\"plugin\\":\\"vite-plugin-checker(TypeScript)\\",\\"stack\\":\\"\\"}]"`;
exports[`typescript serve get initial error and subsequent error 1`] = `"[{\\"frame\\":\\" 4 |/n 5 | function App() {/n > 6 | const [count, setCount] = useState<string>(1)/n | ^/n 7 | return (/n 8 | <div className=/\\"App/\\">/n 9 | <header className=/\\"App-header/\\">\\",\\"id\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"loc\\":{\\"column\\":46,\\"file\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"line\\":6},\\"message\\":\\"Argument of type 'number' is not assignable to parameter of type 'string | (() => string)'.\\",\\"plugin\\":\\"TypeScript\\",\\"stack\\":\\"\\"}]"`;

exports[`typescript serve get initial error and subsequent error 2`] = `
"
Expand All @@ -18,7 +18,7 @@ exports[`typescript serve get initial error and subsequent error 2`] = `
Found 1 error. Watching for file changes."
`;
exports[`typescript serve get initial error and subsequent error 3`] = `"[{\\"frame\\":\\" 4 |/n 5 | function App() {/n > 6 | const [count, setCount] = useState<string>(2)/n | ^/n 7 | return (/n 8 | <div className=/\\"App/\\">/n 9 | <header className=/\\"App-header/\\">\\",\\"id\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"loc\\":{\\"column\\":46,\\"file\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"line\\":6},\\"message\\":\\"Argument of type 'number' is not assignable to parameter of type 'string | (() => string)'.\\",\\"plugin\\":\\"vite-plugin-checker(TypeScript)\\",\\"stack\\":\\"\\"}]"`;
exports[`typescript serve get initial error and subsequent error 3`] = `"[{\\"frame\\":\\" 4 |/n 5 | function App() {/n > 6 | const [count, setCount] = useState<string>(2)/n | ^/n 7 | return (/n 8 | <div className=/\\"App/\\">/n 9 | <header className=/\\"App-header/\\">\\",\\"id\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"loc\\":{\\"column\\":46,\\"file\\":\\"<PROJECT_ROOT>/temp/react-ts/src/App.tsx\\",\\"line\\":6},\\"message\\":\\"Argument of type 'number' is not assignable to parameter of type 'string | (() => string)'.\\",\\"plugin\\":\\"TypeScript\\",\\"stack\\":\\"\\"}]"`;
exports[`typescript serve get initial error and subsequent error 4`] = `
"
Expand Down
4 changes: 2 additions & 2 deletions playground/react-ts/__tests__/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
testDir,
WORKER_CLEAN_TIMEOUT,
} from 'vite-plugin-checker/__tests__/e2e/testUtils'
import { WS_CHECKER_ERROR_TYPE } from 'vite-plugin-checker/src/client'
import { WS_CHECKER_ERROR_EVENT } from 'vite-plugin-checker/src/client'

import { copyCode } from '../../../scripts/jestSetupFilesAfterEnv'
import { serializers } from '../../../scripts/serializers'
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('typescript', () => {
await viteServe({
cwd: testDir,
wsSend: (_payload) => {
if (_payload.type === 'custom' && _payload.event == WS_CHECKER_ERROR_TYPE) {
if (_payload.type === 'custom' && _payload.event == WS_CHECKER_ERROR_EVENT) {
errors = _payload.data.errors
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`eslint serve get initial error and subsequent error 1`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello'/n | ^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"vite-plugin-checker(ESLint)\\",\\"stack\\":\\"\\"}]"`;
exports[`eslint serve get initial error and subsequent error 1`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello'/n | ^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"ESLint\\",\\"stack\\":\\"\\"}]"`;

exports[`eslint serve get initial error and subsequent error 2`] = `
" ERROR(ESLint) Unexpected var, use let or const instead.
Expand All @@ -16,7 +16,7 @@ exports[`eslint serve get initial error and subsequent error 2`] = `
"
`;
exports[`eslint serve get initial error and subsequent error 3`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello~'/n | ^^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"vite-plugin-checker(ESLint)\\",\\"stack\\":\\"\\"}]"`;
exports[`eslint serve get initial error and subsequent error 3`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello~'/n | ^^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"ESLint\\",\\"stack\\":\\"\\"}]"`;
exports[`eslint serve get initial error and subsequent error 4`] = `
" ERROR(ESLint) Unexpected var, use let or const instead.
Expand All @@ -32,7 +32,7 @@ exports[`eslint serve get initial error and subsequent error 4`] = `
"
`;
exports[`eslint serve get initial error and subsequent error 5`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello~'/n | ^^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"vite-plugin-checker(ESLint)\\",\\"stack\\":\\"\\"}]"`;
exports[`eslint serve get initial error and subsequent error 5`] = `"[{\\"frame\\":\\" 1 | import { text } from './text'/n 2 |/n > 3 | var hello = 'Hello~'/n | ^^^^^^^^^^^^^^^^^^^^/n 4 |/n 5 | const rootDom = document.querySelector('#root')!/n 6 | rootDom.innerHTML = hello + text\\",\\"id\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"loc\\":{\\"column\\":1,\\"file\\":\\"<PROJECT_ROOT>/temp/vanilla-ts/src/main.ts\\",\\"line\\":3},\\"message\\":\\"Unexpected var, use let or const instead.\\",\\"plugin\\":\\"ESLint\\",\\"stack\\":\\"\\"}]"`;
exports[`eslint serve get initial error and subsequent error 6`] = `
" ERROR(ESLint) Unexpected var, use let or const instead.
Expand Down
4 changes: 2 additions & 2 deletions playground/vanilla-ts/__tests__/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
testDir,
WORKER_CLEAN_TIMEOUT,
} from '../../../packages/vite-plugin-checker/__tests__/e2e/testUtils'
import { WS_CHECKER_ERROR_TYPE } from '../../../packages/vite-plugin-checker/src/client'
import { WS_CHECKER_ERROR_EVENT } from '../../../packages/vite-plugin-checker/src/client'
import { copyCode } from '../../../scripts/jestSetupFilesAfterEnv'
import { serializers } from '../../../scripts/serializers'

Expand Down Expand Up @@ -45,7 +45,7 @@ describe('eslint', () => {
await viteServe({
cwd: testDir,
wsSend: (_payload) => {
if (_payload.type === 'custom' && _payload.event == WS_CHECKER_ERROR_TYPE) {
if (_payload.type === 'custom' && _payload.event == WS_CHECKER_ERROR_EVENT) {
errors = _payload.data.errors
}
},
Expand Down
1 change: 1 addition & 0 deletions playground/vanilla-ts/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default defineConfig({
plugins: [
reactRefresh(),
checker({
// typescript: true,
eslint: {
lintCommand: 'eslint ./src --ext .ts',
},
Expand Down
Loading

0 comments on commit 7939381

Please sign in to comment.