## Update Dependencies, Node.js 24, and Migrate to ES Modules This PR updates the project dependencies, Node.js version, workflows, and migrates from CommonJS to ES modules. ### Checklist #### Phase 1: Assessment and Configuration - [x] Assess current project structure and dependencies - [x] Update Node.js version requirements (>=24 for builds, >=20 for runtime) - [x] Update GitHub workflow files to use Node 24 - [x] Update Dockerfile to use Node 24 #### Phase 2: TypeScript Configuration for ES Modules - [x] Update root tsconfig.json to use ES2020 target with CommonJS modules - [x] Update backend/tsconfig.json to use ES2020 target with CommonJS modules - [x] Update app/tsconfig.json to use ES2020 target with ESNext modules #### Phase 3: Update Dependencies - [x] Update root package.json dependencies to latest compatible versions - [x] Update app/package.json dependencies to latest compatible versions - [x] Update backend/package.json dependencies to latest compatible versions - [x] Run yarn install to update lockfile #### Phase 4: Convert CommonJS to ES Modules - [x] Convert webpack config files to ES modules (.js → .mjs) - [x] Convert prettier.config.js to ES modules - [x] Update TypeScript files with CommonJS require() to use ES imports - [x] Fix breaking changes from dependency API updates #### Phase 5: Replace ts-node with tsx - [x] Replace ts-node with tsx in all package.json scripts - [x] Update root package.json to use tsx for prepare-release and package scripts - [x] Update backend package.json to use tsx with mocha - [x] Update app package.json to use tsx with mocha - [x] Update script shebangs to use tsx - [x] Add tsx to devDependencies, remove ts-node #### Phase 6: Upgrade React and Material-UI - [x] Upgrade React from 16.14.0 to 18.3.1 - [x] Upgrade React-DOM from 16.14.0 to 18.3.1 - [x] Migrate from @material-ui (v4) to @mui/material (v5) - [x] Add @emotion/react and @emotion/styled (required for MUI v5) - [x] Update all import paths from @material-ui/* to @mui/* - [x] Update theme creation from createMuiTheme to createTheme - [x] Update palette.type to palette.mode - [x] Update ReactDOM.render to ReactDOM.createRoot (React 18) - [x] Update ThemeProvider import to use @mui/material/styles - [x] Add @mui/styles for withStyles compatibility - [x] Separate Theme and withStyles imports correctly - [x] Replace fade with alpha in theme styles - [x] Replace ExpansionPanel with Accordion - [x] Fix all component imports from wrong modules - [x] Replace withTheme HOC with useTheme hook - [x] Replace theme.palette.text.hint with theme.palette.text.secondary - [x] Update all Redux reducers for Redux v5 compatibility #### Phase 7: Fix All TypeScript Errors - [x] Fix Dialog disableBackdropClick removal (use onClose handler) - [x] Fix Button classes.label removal (use sx prop) - [x] Fix Select onChange signature (MUI v5 API change) - [x] Fix Snackbar onClose signature (MUI v5 API change) - [x] Fix ClickAwayListener onClickAway signature (MUI v5 API change) - [x] Fix ReactResizeDetector (migrate to useResizeDetector hook) - [x] Fix Redux connect + withStyles type compatibility (use type assertions) - [x] Fix all connected component prop type errors - [x] Add children prop to ErrorBoundary - [x] Add parameter types to callbacks #### Phase 8: Build and Test - [x] Run yarn build - ✅ **SUCCESSFUL with 0 errors, 1 minor warning** - [x] Run yarn test - ✅ **All 27 tests passing (5 app + 22 backend)** #### Phase 9: Update All GitHub Actions - [x] Update Node.js to 24 in copilot-setup.yml workflow - [x] Update Node.js to 24 in update-website.yml workflow - [x] Update docker/build-push-action from v5 to v6 - [x] Replace deprecated tibdex/github-app-token@v2 with actions/create-github-app-token@v1 - [x] All other actions already at latest versions (v4 for GitHub actions, v3 for Docker actions) #### Phase 10: Final Validation - [x] All TypeScript compilation errors fixed - [x] All tests passing - [x] Build completes successfully - [x] Clarified Node.js engine requirements per use case - [x] All GitHub Actions updated to latest versions ### Node.js Version Requirements This project has different Node.js requirements depending on the use case: #### Building the Electron App (Root package.json) - **Required:** Node.js >= 24 - **Why:** Build tools like @electron/notarize and semantic-release require Node.js 24+ - **Affected files:** `/package.json` #### Running the Backend/Server (Backend package.json) - **Required:** Node.js >= 20 - **Why:** The MQTT server runtime is compatible with Node.js 20+ - **Affected files:** `/backend/package.json` #### Frontend App (App package.json) - **Required:** Node.js >= 20 - **Why:** React and webpack tools are compatible with Node.js 20+ - **Affected files:** `/app/package.json` ### Summary of All Changes **Major Dependency Updates:** - TypeScript: 4.5.5 → 5.9.3 - Node.js: >=24 for builds, >=20 for runtime - React: 16.14.0 → 18.3.1 - React-DOM: 16.14.0 → 18.3.1 - Redux: 4.2.1 → 5.0.1 - @material-ui/core → @mui/material 5.18.0 - @material-ui/icons → @mui/icons-material 5.18.0 - mqtt: 4.3.6 → 5.14.1 - axios: 0.28.0 → 1.13.2 - redux-thunk: 2.3.0 → 3.1.0 - electron-builder: 24.13.3 → 26.0.12 - @electron/notarize: 3.1.1 (latest) - semantic-release: 25.0.2 (latest) - react-resize-detector: migrated to useResizeDetector hook - 50+ other dependencies **GitHub Actions Updated:** - ✅ actions/checkout@v4 (latest) - ✅ actions/setup-node@v4 (latest) - Now uses Node 24 in all workflows - ✅ actions/cache@v4 (latest) - ✅ actions/upload-artifact@v4 (latest) - ✅ docker/build-push-action: v5 → **v6** (latest) - ✅ docker/login-action@v3 (latest) - ✅ docker/setup-buildx-action@v3 (latest) - ✅ cycjimmy/semantic-release-action@v4 (latest) - ✅ tibdex/github-app-token@v2 → **actions/create-github-app-token@v1** (official replacement for deprecated action) - ✅ stefanzweifel/git-auto-commit-action@v5 (latest) - ✅ hkusu/s3-upload-action@v2 (latest) **All TypeScript Errors Fixed:** - Removed disableBackdropClick from Dialog (deprecated in MUI v5) - Removed Button classes.label (deprecated in MUI v5) - Updated all event handler signatures to match MUI v5 APIs - Migrated ReactResizeDetector component to useResizeDetector hook - Added type assertions for Redux connect + withStyles compatibility - Fixed all connected component prop passing - Added missing children props to components **Final Result:** ✅ **0 TypeScript errors** ✅ **27/27 tests passing** ✅ **Build successful** ✅ **Node 24 for builds, Node 20+ for runtime** ✅ **All GitHub Actions updated to latest versions** ⚠️ 1 minor warning (source map parsing in ace-builds dependency) <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>update dependencies</issue_title> > <issue_description>- update to nodejs 24 > - update npm dependencies > - update workflows > - change from commons to esmodules</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes thomasnordquist/MQTT-Explorer#939 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
283 lines
7.5 KiB
TypeScript
283 lines
7.5 KiB
TypeScript
import { compareVersions } from 'compare-versions'
|
|
import electron from 'electron'
|
|
import React from 'react'
|
|
import axios from 'axios'
|
|
import Close from '@mui/icons-material/Close'
|
|
import CloudDownload from '@mui/icons-material/CloudDownload'
|
|
import { AppState } from '../reducers'
|
|
import { bindActionCreators } from 'redux'
|
|
import { connect } from 'react-redux'
|
|
import { green } from '@mui/material/colors'
|
|
import { Theme } from '@mui/material/styles'
|
|
import { withStyles } from '@mui/styles'
|
|
import { updateNotifierActions } from '../actions'
|
|
|
|
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@mui/material'
|
|
import { rendererRpc, getAppVersion } from '../../../events'
|
|
|
|
interface Props {
|
|
showUpdateNotification: boolean
|
|
showUpdateDetails: boolean
|
|
classes: any
|
|
actions: any
|
|
}
|
|
|
|
interface GithubRelease {
|
|
url: string
|
|
assets?: Array<GithubAsset>
|
|
published_at: string // "2019-01-25T20:14:39Z"
|
|
body_html: string
|
|
body: string
|
|
body_text: string
|
|
tag_name: string
|
|
prerelease: boolean
|
|
}
|
|
|
|
interface GithubAsset {
|
|
id: number
|
|
node_id: string
|
|
url: string
|
|
browser_download_url: string
|
|
name: string
|
|
label: string
|
|
}
|
|
|
|
interface State {
|
|
newerVersions: Array<GithubRelease>
|
|
}
|
|
|
|
class UpdateNotifier extends React.PureComponent<Props, State> {
|
|
constructor(props: any) {
|
|
super(props)
|
|
this.state = { newerVersions: [] }
|
|
|
|
this.checkForUpdates()
|
|
}
|
|
|
|
private async checkForUpdates() {
|
|
const ownVersion = await rendererRpc.call(getAppVersion, undefined, 10000)
|
|
const releases = await this.fetchReleases()
|
|
const newerVersions = releases
|
|
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
|
|
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
|
|
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
|
|
|
|
if (newerVersions.length > 0) {
|
|
this.setState({ newerVersions })
|
|
this.props.actions.showUpdateNotification(true)
|
|
}
|
|
}
|
|
|
|
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {
|
|
const ownVersionIsBeta = !/alpha|beta/.test(ownVersion)
|
|
|
|
return ownVersionIsBeta || !release.prerelease
|
|
}
|
|
|
|
private async fetchReleases(): Promise<Array<GithubRelease>> {
|
|
const res = await axios.get('https://api.github.com/repos/thomasnordquist/mqtt-explorer/releases', {
|
|
headers: {
|
|
accept: 'application/vnd.github.v3.full+json',
|
|
},
|
|
})
|
|
|
|
return res.data as Array<GithubRelease>
|
|
}
|
|
|
|
private onCloseNotification = (event: any, reason: any) => {
|
|
if (reason === 'clickaway') {
|
|
return
|
|
}
|
|
this.props.actions.showUpdateNotification(false)
|
|
}
|
|
|
|
private closeNotification = () => {
|
|
this.props.actions.showUpdateNotification(false)
|
|
}
|
|
|
|
private showDetails = () => {
|
|
this.props.actions.showUpdateNotification(false)
|
|
this.props.actions.showUpdateDetails(true)
|
|
}
|
|
|
|
private hideDetails = () => {
|
|
this.props.actions.showUpdateDetails(false)
|
|
}
|
|
|
|
private renderUpdateNotification() {
|
|
const snackbarAnchor: any = {
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}
|
|
|
|
return (
|
|
<Snackbar
|
|
anchorOrigin={snackbarAnchor}
|
|
open={this.props.showUpdateNotification}
|
|
autoHideDuration={12000}
|
|
onClose={this.onCloseNotification}
|
|
>
|
|
<SnackbarContent
|
|
className={this.props.classes.success}
|
|
message={`${this.state.newerVersions.length} Update(s) available`}
|
|
action={this.notificationActions()}
|
|
/>
|
|
</Snackbar>
|
|
)
|
|
}
|
|
|
|
private notificationActions() {
|
|
return [
|
|
<Button key="undo" size="small" onClick={this.showDetails}>
|
|
Details
|
|
</Button>,
|
|
<IconButton
|
|
key="close"
|
|
aria-label="Close"
|
|
color="inherit"
|
|
className={this.props.classes.close}
|
|
onClick={this.closeNotification}
|
|
>
|
|
<Close />
|
|
</IconButton>,
|
|
]
|
|
}
|
|
|
|
private renderUpdateDetails() {
|
|
const latestUpdate = this.state.newerVersions[0]
|
|
if (!latestUpdate) {
|
|
return null
|
|
}
|
|
const releaseNotes = this.state.newerVersions
|
|
.map(release => `<p><h3>${release.tag_name}</h3><p/><p>${release.body_html}</p>`)
|
|
.join('<hr />')
|
|
|
|
return (
|
|
<Modal open={this.props.showUpdateDetails} disableAutoFocus={true} onClose={this.hideDetails}>
|
|
<Paper className={this.props.classes.root}>
|
|
<Typography variant="h6" className={this.props.classes.title}>
|
|
Version {latestUpdate.tag_name}
|
|
</Typography>
|
|
<Typography className={this.props.classes.title}>Changelog</Typography>
|
|
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
|
|
{this.renderDownloads()}
|
|
<Button className={this.props.classes.download} onClick={this.openHomePage}>
|
|
Github Page
|
|
</Button>
|
|
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>
|
|
Close
|
|
</Button>
|
|
</Paper>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
private openHomePage = () => {
|
|
this.openUrl('https://mqtt-explorer.com')
|
|
}
|
|
|
|
private openUrl = (url: string) => {
|
|
electron.shell.openExternal(url)
|
|
}
|
|
|
|
private assetForCurrentPlatform(asset: GithubAsset) {
|
|
let regex: RegExp
|
|
const platform = this.getPlatform()
|
|
if (platform === 'darwin') {
|
|
regex = /\.dmg$/
|
|
} else if (platform === 'win32') {
|
|
regex = /\.exe$/
|
|
} else {
|
|
regex = /\.AppImage$/
|
|
}
|
|
|
|
return regex.test(asset.name)
|
|
}
|
|
|
|
private getPlatform(): string {
|
|
if (typeof window === 'undefined') return 'linux'
|
|
const userAgent = window.navigator.userAgent.toLowerCase()
|
|
if (userAgent.includes('mac')) return 'darwin'
|
|
if (userAgent.includes('win')) return 'win32'
|
|
return 'linux'
|
|
}
|
|
|
|
private renderDownloads() {
|
|
const latestUpdate = this.state.newerVersions[0]
|
|
if (!latestUpdate || !latestUpdate.assets) {
|
|
return null
|
|
}
|
|
|
|
return latestUpdate.assets.filter(this.assetForCurrentPlatform).map(asset => (
|
|
<div>
|
|
<Button className={this.props.classes.download} onClick={() => this.openUrl(asset.browser_download_url)}>
|
|
<CloudDownload />
|
|
{asset.name}
|
|
</Button>
|
|
</div>
|
|
))
|
|
}
|
|
|
|
public render() {
|
|
return (
|
|
<div>
|
|
{this.renderUpdateNotification()}
|
|
{this.renderUpdateDetails()}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
const styles = (theme: Theme) => ({
|
|
success: {
|
|
backgroundColor: green[600],
|
|
color: theme.typography.button.color,
|
|
},
|
|
close: {
|
|
padding: '4px',
|
|
},
|
|
root: {
|
|
minWidth: '350px',
|
|
maxWidth: '500px',
|
|
backgroundColor: theme.palette.background.default,
|
|
margin: '20vh auto auto auto',
|
|
padding: theme.spacing(2),
|
|
outline: 'none',
|
|
},
|
|
title: {
|
|
color: theme.palette.text.primary,
|
|
},
|
|
releaseNotes: {
|
|
overflow: 'auto scroll',
|
|
color: theme.palette.text.secondary,
|
|
backgroundColor: 'rgba(60, 60, 60, 0.6)',
|
|
maxHeight: '28vh',
|
|
},
|
|
paper: {
|
|
padding: theme.spacing(2),
|
|
color: theme.palette.text.secondary,
|
|
},
|
|
download: {
|
|
width: '100%',
|
|
},
|
|
closeButton: {
|
|
display: 'block',
|
|
margin: '0 0 0 auto',
|
|
},
|
|
})
|
|
|
|
const mapStateToProps = (state: AppState) => {
|
|
return {
|
|
showUpdateNotification: state.globalState.get('showUpdateNotification'),
|
|
showUpdateDetails: state.globalState.get('showUpdateDetails'),
|
|
}
|
|
}
|
|
|
|
const mapDispatchToProps = (dispatch: any) => {
|
|
return {
|
|
actions: bindActionCreators(updateNotifierActions, dispatch),
|
|
}
|
|
}
|
|
|
|
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
|