Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show version staleness in error overlay #44234

Merged
merged 51 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
33c79d5
feat: show version staleness in error overlay
balazsorban44 Dec 21, 2022
f5ed37b
move comparison to server
balazsorban44 Dec 21, 2022
97a59e5
add fallback title
balazsorban44 Dec 21, 2022
529953a
add comparison logic
balazsorban44 Dec 21, 2022
56faefc
rephrase, add link
balazsorban44 Dec 21, 2022
d9dc427
rephrase
balazsorban44 Dec 21, 2022
57093ac
add unknown state info
balazsorban44 Dec 21, 2022
de8472b
add message page
balazsorban44 Dec 21, 2022
33b84ac
remove unused variable
balazsorban44 Dec 21, 2022
5de1dcd
change default state to unknown
balazsorban44 Dec 21, 2022
0bd318d
assume cached endpoint
balazsorban44 Dec 21, 2022
d856b93
remove todo
balazsorban44 Dec 21, 2022
e072b87
Update packages/next/client/components/react-dev-overlay/internal/con…
balazsorban44 Jan 12, 2023
2875ee7
fix todo
balazsorban44 Jan 12, 2023
1664437
Merge branch 'canary' into feat/staleness-in-error-overlay
balazsorban44 Jan 12, 2023
3a8375a
revert
balazsorban44 Jan 12, 2023
24e2442
revert
balazsorban44 Jan 12, 2023
8c625f5
address review
balazsorban44 Jan 12, 2023
5907c7a
Update errors/version-staleness.md
balazsorban44 Jan 12, 2023
03263c6
simplify error message
balazsorban44 Jan 12, 2023
7229346
Merge branch 'canary' into feat/staleness-in-error-overlay
balazsorban44 Jan 12, 2023
59229e6
Merge branch 'canary' into feat/staleness-in-error-overlay
balazsorban44 Jan 19, 2023
b7f1c20
Apply suggestions from code review
balazsorban44 Jan 19, 2023
aec87d8
fix import
balazsorban44 Jan 19, 2023
5d05fad
show correct installed version if fetch fails
balazsorban44 Jan 19, 2023
932961f
fix test
balazsorban44 Jan 19, 2023
073cfa6
Merge branch 'canary' of https://github.com/hanneslund/next.js into f…
Feb 3, 2023
6292f75
Add null check
Feb 3, 2023
4a492f7
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 3, 2023
606b431
Fix lint?
Feb 3, 2023
0b7babb
Fix parseVersionInfo
Feb 3, 2023
1cb3b4e
Apply suggestions from code review
styfle Feb 7, 2023
6a061ba
Update version-staleness.md
balazsorban44 Feb 8, 2023
f89337d
extract and use retrieving registry logic
balazsorban44 Feb 8, 2023
0d48a6c
Check if versionInfo is provided
Feb 10, 2023
65e8445
Move staleness indicator to seperate file
Feb 10, 2023
0ef83c8
Add staleness info to build errors and runtime errors
Feb 10, 2023
6b19f22
Fix types
Feb 10, 2023
a35bff5
Add tests
Feb 10, 2023
cdfdfa8
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 10, 2023
d2bd162
Fix ts errors
Feb 10, 2023
8f7c535
Fix another ts error
Feb 10, 2023
83c12cb
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 16, 2023
add39c8
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 16, 2023
aadb826
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 17, 2023
27be20d
Don't fetch staleness info if telemetry is disabled
Feb 17, 2023
f79dbbb
Reuse telemetry instance
Feb 17, 2023
5c7226c
Enable version staleness in tests
Feb 17, 2023
90c6b0c
Move version info parse function to its own file and colocate unit test
Feb 17, 2023
94eff66
Merge branch 'canary' into feat/staleness-in-error-overlay
hanneslund Feb 17, 2023
dcf268e
Merge branch 'canary' into feat/staleness-in-error-overlay
kodiakhq[bot] Feb 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import React, {
import stripAnsi from 'next/dist/compiled/strip-ansi'
import formatWebpackMessages from '../../dev/error-overlay/format-webpack-messages'
import { useRouter } from '../navigation'
import { errorOverlayReducer } from './internal/error-overlay-reducer'
import {
ACTION_VERSION_INFO,
errorOverlayReducer,
} from './internal/error-overlay-reducer'
import {
ACTION_BUILD_OK,
ACTION_BUILD_ERROR,
Expand All @@ -31,9 +34,12 @@ import {
useWebsocketPing,
} from './internal/helpers/use-websocket'

import type { VersionInfo } from './internal/container/Errors'

interface Dispatcher {
onBuildOk(): void
onBuildError(message: string): void
onVersionInfo(versionInfo: VersionInfo): void
onBeforeRefresh(): void
onRefresh(): void
}
Expand Down Expand Up @@ -218,7 +224,8 @@ function processMessage(
handleAvailableHash(obj.hash)
}

const { errors, warnings } = obj
const { errors, warnings, versionInfo } = obj
dispatcher.onVersionInfo(versionInfo)
const hasErrors = Boolean(errors && errors.length)
// Compilation with errors (e.g. syntax error or missing modules).
if (hasErrors) {
Expand Down Expand Up @@ -376,6 +383,7 @@ function processMessage(
router.refresh()
return
}

case 'pong': {
const { invalid } = obj
if (invalid) {
Expand Down Expand Up @@ -403,21 +411,25 @@ export default function HotReload({
buildError: null,
errors: [],
refreshState: { type: 'idle' },
versionInfo: { installed: '0.0.0', staleness: 'fresh' },
})
const dispatcher = useMemo((): Dispatcher => {
return {
onBuildOk(): void {
onBuildOk() {
dispatch({ type: ACTION_BUILD_OK })
},
onBuildError(message: string): void {
onBuildError(message) {
dispatch({ type: ACTION_BUILD_ERROR, message })
},
onBeforeRefresh(): void {
onBeforeRefresh() {
dispatch({ type: ACTION_BEFORE_REFRESH })
},
onRefresh(): void {
onRefresh() {
dispatch({ type: ACTION_REFRESH })
},
onVersionInfo(versionInfo) {
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
},
}
}, [dispatch])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
import { ShadowPortal } from './components/ShadowPortal'
import { BuildError } from './container/BuildError'
import { Errors, SupportedErrorEvent } from './container/Errors'
import { RootLayoutError } from './container/RootLayoutError'
import { parseStack } from './helpers/parseStack'
import { Base } from './styles/Base'
import { ComponentStyles } from './styles/ComponentStyles'
import { CssReset } from './styles/CssReset'
import { parseStack } from './helpers/parseStack'
import { RootLayoutError } from './container/RootLayoutError'

interface ReactDevOverlayState {
reactError: SupportedErrorEvent | null
Expand Down Expand Up @@ -73,15 +73,18 @@ class ReactDevOverlay extends React.PureComponent<
<CssReset />
<Base />
<ComponentStyles />

{rootLayoutMissingTagsError ? (
<RootLayoutError
missingTags={rootLayoutMissingTagsError.missingTags}
/>
) : hasBuildError ? (
<BuildError message={state.buildError!} />
) : reactError ? (
<Errors initialDisplayState="fullscreen" errors={[reactError]} />
<Errors
versionInfo={state.versionInfo}
initialDisplayState="fullscreen"
errors={[reactError]}
/>
) : hasRuntimeErrors ? (
<Errors initialDisplayState="minimized" errors={state.errors} />
) : undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const styles = css`
align-items: center;
justify-content: space-between;
}
[data-nextjs-dialog-left-right] > nav {
flex: 1;
display: flex;
align-items: center;
margin-right: var(--size-gap);
}
[data-nextjs-dialog-left-right] > nav > button {
display: inline-flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type SupportedErrorEvent = {
export type ErrorsProps = {
errors: SupportedErrorEvent[]
initialDisplayState: DisplayState
versionInfo?: VersionInfo
}

type ReadyErrorEvent = ReadyRuntimeError
Expand Down Expand Up @@ -78,9 +79,22 @@ const HotlinkedText: React.FC<{
)
}

const stalenessTitles = {
fresh: 'Next.js is up to date!',
stale: 'Next.js is out of date, update recommended!',
outdated: 'Next.js is out of date, update necessary!',
unknown: 'Could not determine Next.js version',
}

export interface VersionInfo {
installed: string
staleness: keyof typeof stalenessTitles
}

export const Errors: React.FC<ErrorsProps> = function Errors({
errors,
initialDisplayState,
versionInfo,
}) {
const [lookups, setLookups] = React.useState(
{} as { [eventId: string]: ReadyErrorEvent }
Expand Down Expand Up @@ -271,6 +285,13 @@ export const Errors: React.FC<ErrorsProps> = function Errors({
<span>{readyErrors.length}</span> unhandled error
{readyErrors.length < 2 ? '' : 's'}
</small>
<small
title={stalenessTitles[versionInfo!.staleness]}
className="nextjs-container-build-error-version-status"
>
Next.js {versionInfo!.installed}
<span className={versionInfo!.staleness} />
</small>
</LeftRightDialogHeader>
<h1 id="nextjs__container_errors_label">
{isServerError ? 'Server Error' : 'Unhandled Runtime Error'}
Expand Down Expand Up @@ -365,4 +386,25 @@ export const styles = css`
.nextjs-toast-errors-hide-button:hover {
opacity: 1;
}
.nextjs-container-build-error-version-status {
flex: 1;
text-align: right;
}
.nextjs-container-build-error-version-status span {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
background: #eaeaea;
margin-left: var(--size-gap);
}
.nextjs-container-build-error-version-status span.fresh {
background: #50e3c2;
}
.nextjs-container-build-error-version-status span.outdated {
background: #e00;
}
.nextjs-container-build-error-version-status span.stale {
background: #f5a623;
}
`
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import { SupportedErrorEvent } from './container/Errors'
import type { SupportedErrorEvent, VersionInfo } from './container/Errors'

export const ACTION_BUILD_OK = 'build-ok'
export const ACTION_BUILD_ERROR = 'build-error'
export const ACTION_BEFORE_REFRESH = 'before-fast-refresh'
export const ACTION_REFRESH = 'fast-refresh'
export const ACTION_UNHANDLED_ERROR = 'unhandled-error'
export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
export const ACTION_VERSION_INFO = 'version-info'

interface BuildOkAction {
type: typeof ACTION_BUILD_OK
Expand All @@ -32,6 +33,11 @@ export interface UnhandledRejectionAction {
frames: StackFrame[]
}

interface VersionInfoAction {
type: typeof ACTION_VERSION_INFO
versionInfo: VersionInfo
}

export type FastRefreshState =
| {
type: 'idle'
Expand All @@ -49,6 +55,7 @@ export interface OverlayState {
missingTags: string[]
}
refreshState: FastRefreshState
versionInfo: VersionInfo
}

function pushErrorFilterDuplicates(
Expand All @@ -64,17 +71,18 @@ function pushErrorFilterDuplicates(
]
}

export function errorOverlayReducer(
state: Readonly<OverlayState>,
action: Readonly<
export const errorOverlayReducer: React.Reducer<
Readonly<OverlayState>,
Readonly<
| BuildOkAction
| BuildErrorAction
| BeforeFastRefreshAction
| FastRefreshAction
| UnhandledErrorAction
| UnhandledRejectionAction
| VersionInfoAction
>
): OverlayState {
> = (state, action) => {
switch (action.type) {
case ACTION_BUILD_OK: {
return { ...state, buildError: null }
Expand Down Expand Up @@ -134,6 +142,9 @@ export function errorOverlayReducer(
return state
}
}
case ACTION_VERSION_INFO: {
return { ...state, versionInfo: action.versionInfo }
}
default: {
return state
}
Expand Down
6 changes: 5 additions & 1 deletion packages/next/server/dev/hot-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack'
import type ws from 'ws'
import { isMiddlewareFilename } from '../../build/utils'
import { nonNullable } from '../../lib/non-nullable'
import type { VersionInfo } from '../../client/components/react-dev-overlay/internal/container/Errors'

function isMiddlewareStats(stats: webpack.Stats) {
for (const key of stats.compilation.entrypoints.keys()) {
Expand Down Expand Up @@ -85,13 +86,15 @@ export class WebpackHotMiddleware {
middlewareLatestStats: { ts: number; stats: webpack.Stats } | null
serverLatestStats: { ts: number; stats: webpack.Stats } | null
closed: boolean
versionInfo: VersionInfo

constructor(compilers: webpack.Compiler[]) {
constructor(compilers: webpack.Compiler[], versionInfo: VersionInfo) {
this.eventStream = new EventStream()
this.clientLatestStats = null
this.middlewareLatestStats = null
this.serverLatestStats = null
this.closed = false
this.versionInfo = versionInfo

compilers[0].hooks.invalid.tap(
'webpack-hot-middleware',
Expand Down Expand Up @@ -182,6 +185,7 @@ export class WebpackHotMiddleware {
...(stats.warnings || []),
...(middlewareStats.warnings || []),
],
versionInfo: this.versionInfo,
})
}
}
Expand Down
47 changes: 46 additions & 1 deletion packages/next/server/dev/hot-reloader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { webpack, StringXor } from 'next/dist/compiled/webpack/webpack'
import * as semver from 'next/dist/compiled/semver'
import type { NextConfigComplete } from '../config-shared'
import type { CustomRoutes } from '../../lib/load-custom-routes'
import { getOverlayMiddleware } from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware'
Expand Down Expand Up @@ -47,6 +48,7 @@ import ws from 'next/dist/compiled/ws'
import { promises as fs } from 'fs'
import { getPageStaticInfo } from '../../build/analysis/get-page-static-info'
import { UnwrapPromise } from '../../lib/coalesced-function'
import type { VersionInfo } from '../../client/components/react-dev-overlay/internal/container/Errors'

function diff(a: Set<any>, b: Set<any>) {
return new Set([...a].filter((v) => !b.has(v)))
Expand Down Expand Up @@ -178,6 +180,10 @@ export default class HotReloader {
private hotReloaderSpan: Span
private pagesMapping: { [key: string]: string } = {}
private appDir?: string
private versionInfo: VersionInfo = {
staleness: 'unknown',
installed: '0.0.0',
}
public multiCompiler?: webpack.MultiCompiler
public activeConfigs?: Array<
UnwrapPromise<ReturnType<typeof getBaseWebpackConfig>>
Expand Down Expand Up @@ -414,6 +420,42 @@ export default class HotReloader {
)
}

private async getVersionInfo(span: Span) {
const versionInfoSpan = span.traceChild('get-version-info')
return versionInfoSpan.traceAsyncFn<VersionInfo>(async () => {
// TODO: Get the correct values
const installed = semver.parse('13.0.6')
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved
const latest = semver.parse('13.0.8')
const canary = semver.parse('13.0.8-canary.8')

let staleness: VersionInfo['staleness'] = 'unknown'

if (installed && latest && canary) {
if (installed.major < latest.major) {
// Old version
staleness = 'outdated'
} else if (
// Canary, but old
installed.prerelease[0] === 'canary' &&
semver.lt(installed, canary)
) {
staleness = 'outdated'
} else if (
// Stable, but not the latest
!installed.prerelease.length &&
semver.lt(installed, latest)
) {
staleness = 'stale'
} else {
// Latest
staleness = 'fresh'
}
}

return { installed: installed?.raw ?? '0.0.0', staleness }
})
}

private async getWebpackConfig(span: Span) {
const webpackConfigSpan = span.traceChild('get-webpack-config')

Expand Down Expand Up @@ -558,6 +600,8 @@ export default class HotReloader {
const startSpan = this.hotReloaderSpan.traceChild('start')
startSpan.stop() // Stop immediately to create an artificial parent span

this.versionInfo = await this.getVersionInfo(startSpan)

await this.clean(startSpan)
// Ensure distDir exists before writing package.json
await fs.mkdir(this.distDir, { recursive: true })
Expand Down Expand Up @@ -1039,7 +1083,8 @@ export default class HotReloader {
)

this.webpackHotMiddleware = new WebpackHotMiddleware(
this.multiCompiler.compilers
this.multiCompiler.compilers,
this.versionInfo
)

let booted = false
Expand Down