refactors part of the frontend build pipeline, introduces ts

This commit is contained in:
Lars Berning 2022-03-03 22:22:19 +01:00
parent 156591fb51
commit c9e7b310b4
51 changed files with 781 additions and 5566 deletions

View File

@ -1,18 +1,18 @@
{
"env": {
"browser": false,
"node": true,
"es2021": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"ignorePatterns": ["**/*.min.js"],
"rules": {
"camelcase": 0
}
"env": {
"browser": false,
"node": true,
"es2021": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"ignorePatterns": ["**/*.min.js"],
"rules": {
"camelcase": 0
}
}

View File

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"node": false,
"es2021": true
},
"extends": [
"standard",
"plugin:wc/recommended",
"plugin:lit/recommended"
],
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"ignorePatterns": ["**/*.min.js"],
"rules": {
"camelcase": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 636 B

View File

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 984 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -7,11 +6,12 @@
import { customElement, property } from 'lit/decorators.js'
import { buttonStyles } from '../lib/styles.js'
import { createRef, ref } from 'lit/directives/ref.js'
import { AppElement, css, html } from './AppElement.js'
import { createRef, ref, Ref } from 'lit/directives/ref.js'
import { AppElement, css, html } from './AppElement'
@customElement('app-dialog')
export class AppDialog extends AppElement {
dialog: Ref<Element>
constructor () {
super()
this.dialog = createRef()
@ -58,7 +58,7 @@ export class AppDialog extends AppElement {
}
@property({ type: Boolean, reflect: true })
dialogOpen
dialogOpen: boolean = false
render () {
return html`
@ -76,7 +76,7 @@ export class AppDialog extends AppElement {
`
}
close (event) {
close (event: any) {
if (event.target.returnValue !== 'confirm') {
this.dispatchEvent(new CustomEvent('close', { detail: 'cancel' }))
} else {
@ -89,7 +89,7 @@ export class AppDialog extends AppElement {
this.dialog.value.showModal()
}
updated (changedProperties) {
updated (changedProperties: Map<string, object>) {
if (changedProperties.has('dialogOpen')) {
if (this.dialogOpen) {
// @ts-ignore

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -7,14 +6,14 @@
import { LitElement } from 'lit'
import { property } from 'lit/decorators.js'
import { APP_STATE } from '../store/appState.js'
import { AppState, APP_STATE } from '../store/appState.js'
export * from 'lit'
export class AppElement extends LitElement {
// this is how we implement a global state: a global state object is passed via properties
// to child components
@property({ type: Object })
appState = APP_STATE
appState: AppState = APP_STATE
// ..and state changes are send back to the root component of the app by dispatching
// a CustomEvent
@ -23,7 +22,7 @@ export class AppElement extends LitElement {
}
// a helper to dispatch events to the parent components
sendEvent (eventType, eventData) {
sendEvent (eventType: string, eventData: object) {
this.dispatchEvent(
new CustomEvent(eventType, {
detail: eventData,

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -6,7 +5,7 @@
*/
import { customElement, property } from 'lit/decorators.js'
import { AppElement, css, svg } from './AppElement.js'
import { AppElement, css, svg } from './AppElement'
@customElement('battery-icon')
export class DashboardMetric extends AppElement {
@ -22,15 +21,15 @@ export class DashboardMetric extends AppElement {
`
}
@property({ type: String })
batteryLevel = ''
@property({ type: Number })
batteryLevel = 0
render () {
// 416 is the max width value of the battery bar in the SVG graphic
const batteryWidth = parseInt(this.batteryLevel) * 416 / 100
const batteryWidth = this.batteryLevel * 416 / 100
// if battery level is low, highlight the battery icon
const iconClass = parseInt(this.batteryLevel) > 25 ? 'icon' : 'icon low-battery'
const iconClass = this.batteryLevel > 25 ? 'icon' : 'icon low-battery'
return svg`
<svg aria-hidden="true" focusable="false" class="${iconClass}" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -8,8 +7,8 @@
import { customElement, state } from 'lit/decorators.js'
import { icon_bluetooth, icon_compress, icon_expand, icon_gamepad, icon_poweroff, icon_undo, icon_upload } from '../lib/icons.js'
import { buttonStyles } from '../lib/styles.js'
import './AppDialog.js'
import { AppElement, css, html } from './AppElement.js'
import './AppDialog'
import { AppElement, css, html, TemplateResult } from './AppElement'
@customElement('dashboard-actions')
export class DashboardActions extends AppElement {
static get styles () {
@ -41,7 +40,7 @@ export class DashboardActions extends AppElement {
}
@state()
dialog
dialog: TemplateResult<1> | undefined = undefined
render () {
return html`
@ -121,34 +120,34 @@ export class DashboardActions extends AppElement {
}
uploadTraining () {
const dialogClosed = (event: any) => {
this.dialog = undefined
if (event.detail === 'confirm') {
this.sendEvent('triggerAction', { command: 'uploadTraining' })
}
}
this.dialog = html`
<app-dialog @close=${dialogClosed}>
<legend>${icon_upload}<br/>Upload to Strava?</legend>
<p>Do you want to finish your workout and upload it to Strava?</p>
</app-dialog>
`
function dialogClosed (event) {
this.dialog = undefined
if (event.detail === 'confirm') {
// @ts-ignore
this.sendEvent('triggerAction', { command: 'uploadTraining' })
}
}
}
shutdown () {
const dialogClosed = (event: any) => {
this.dialog = undefined
if (event.detail === 'confirm') {
this.sendEvent('triggerAction', { command: 'shutdown' })
}
}
this.dialog = html`
<app-dialog @close=${dialogClosed}>
<legend>${icon_poweroff}<br/>Shutdown Open Rowing Monitor?</legend>
<p>Do you want to shutdown the device?</p>
</app-dialog>
`
function dialogClosed (event) {
this.dialog = undefined
if (event.detail === 'confirm') {
// @ts-ignore
this.sendEvent('triggerAction', { command: 'shutdown' })
}
}
}
}

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -6,7 +5,7 @@
*/
import { customElement, property } from 'lit/decorators.js'
import { AppElement, css, html } from './AppElement.js'
import { AppElement, css, html, svg } from './AppElement'
@customElement('dashboard-metric')
export class DashboardMetric extends AppElement {
@ -37,7 +36,7 @@ export class DashboardMetric extends AppElement {
}
@property({ type: Object })
icon
icon = svg``
@property({ type: String })
unit = ''

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -11,8 +10,10 @@ import { icon_bolt, icon_exit, icon_heartbeat, icon_paddle, icon_route, icon_sto
import { metricValue, metricUnit } from '../lib/helper.js'
import { buttonStyles } from '../lib/styles.js'
import { AppElement, css, html } from './AppElement.js'
import { AppState } from '../store/appState.js'
@customElement('game-component')
export class GameComponent extends AppElement {
rowingGames: any
static get styles () {
return [
buttonStyles,
@ -134,10 +135,10 @@ export class GameComponent extends AppElement {
// This problem only occurs, when the update events are created from a web request (i.e. by receiving
// new rowing metrics via web socket).
// By delivering the app state updates directly here from index.js, this problem does not occur.
this.sendEvent('setGameStateUpdater', (appState) => { this.gameAppState(appState) })
this.sendEvent('setGameStateUpdater', (appState: AppState) => { this.gameAppState(appState) })
}
gameAppState (appState) {
gameAppState (appState: AppState) {
if (this.rowingGames) this.rowingGames.appState(appState)
}

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -9,10 +8,10 @@ import { customElement, property } from 'lit/decorators.js'
import { metricUnit, metricValue } from '../lib/helper.js'
import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons.js'
import { APP_STATE } from '../store/appState.js'
import { AppElement, css, html } from './AppElement.js'
import './BatteryIcon.js'
import './DashboardActions.js'
import './DashboardMetric.js'
import { AppElement, css, html } from './AppElement'
import './BatteryIcon'
import './DashboardActions'
import './DashboardMetric'
@customElement('performance-dashboard')
export class PerformanceDashboard extends AppElement {
@ -48,9 +47,6 @@ export class PerformanceDashboard extends AppElement {
`
}
@property({ type: Object })
appState = APP_STATE
render () {
const metrics = this.appState.metrics
return html`
@ -58,12 +54,12 @@ export class PerformanceDashboard extends AppElement {
<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metricValue(metrics, 'splitFormatted')}></dashboard-metric>
<dashboard-metric .icon=${icon_bolt} unit="watt" .value=${metricValue(metrics, 'power')}></dashboard-metric>
<dashboard-metric .icon=${icon_paddle} unit="/min" .value=${metricValue(metrics, 'strokesPerMinute')}></dashboard-metric>
${metrics?.heartrate?.value
${metrics?.heartrate
? html`
<dashboard-metric .icon=${icon_heartbeat} unit="bpm" .value=${metricValue(metrics, 'heartrate')}>
${metrics?.heartrateBatteryLevel?.value
${metrics?.heartrateBatteryLevel
? html`
<battery-icon .batteryLevel=${metrics?.heartrateBatteryLevel?.value}></battery-icon>
<battery-icon .batteryLevel=${metrics?.heartrateBatteryLevel}></battery-icon>
`
: ''
}

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,4 +1,3 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
@ -10,12 +9,17 @@ import { customElement, state } from 'lit/decorators.js'
import './components/GameComponent.js'
import './components/PerformanceDashboard.js'
import { createApp } from './lib/app.js'
import { APP_STATE } from './store/appState.js'
import { AppState, APP_STATE } from './store/appState.js'
@customElement('web-app')
export class App extends LitElement {
@state()
appState = APP_STATE
appState: AppState = APP_STATE
private app: {
handleAction: (action: object) => void
}
private gameStateUpdater?: (state: AppState) => void
constructor () {
super()
@ -51,9 +55,9 @@ export class App extends LitElement {
* the global state is updated by replacing the appState with a copy of the new state
* todo: maybe it is more convenient to just pass the state elements that should be changed?
* i.e. do something like this.appState = { ..this.appState, ...newState }
* @param {Object} newState the new state of the application
* @param {AppState} newState the new state of the application
*/
updateState = (newState) => {
updateState = (newState: AppState) => {
this.appState = { ...newState }
// notify games about new app state
if (this.gameStateUpdater) this.gameStateUpdater(this.appState)

View File

@ -0,0 +1,85 @@
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Defines the global state of the app
*/
export interface Metrics {
sessionState: string
durationTotal: number
durationTotalFormatted: string
strokesTotal: number
distanceTotal: number
caloriesTotal: number
caloriesPerMinute: number
caloriesPerHour: number
strokeTime: number
distance: number
power: number
powerRaw: number
split: number
splitFormatted: string
powerRatio: number
instantaneousTorque: number
strokesPerMinute: number
speed: number
strokeState: string
heartrate: number
heartrateBatteryLevel: number
}
export interface AppState {
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
appMode: string
// currently can be DASHBOARD or 'ROWINGGAMES'
activeRoute: string
// contains all the rowing metrics that are delivered from the backend
metrics: Metrics,
config: {
// currently can be FTMS, FTMSBIKE or PM5
peripheralMode: string
// true if upload to strava is enabled
stravaUploadEnabled: boolean
// true if remote device shutdown is enabled
shutdownEnabled: boolean
}
}
export const APP_STATE: AppState = {
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
appMode: '',
// currently can be DASHBOARD or 'ROWINGGAMES'
activeRoute: 'DASHBOARD',
// contains all the rowing metrics that are delivered from the backend
metrics: {
sessionState: 'waitingForStart',
durationTotal: 0,
durationTotalFormatted: '--',
strokesTotal: 0,
distanceTotal: 0,
caloriesTotal: 0,
caloriesPerMinute: 0,
caloriesPerHour: 0,
strokeTime: 0,
distance: 0,
power: 0,
powerRaw: 0,
split: Infinity,
splitFormatted: '--',
powerRatio: 0,
instantaneousTorque: 0,
strokesPerMinute: 0,
speed: 0,
strokeState: 'RECOVERY',
heartrate: 0,
heartrateBatteryLevel: 0,
},
config: {
// currently can be FTMS, FTMSBIKE or PM5
peripheralMode: '',
// true if upload to strava is enabled
stravaUploadEnabled: false,
// true if remote device shutdown is enabled
shutdownEnabled: false
}
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": false,
"esModuleInterop": false,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ES2020",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}

View File

@ -1,21 +0,0 @@
{
"env": {
"browser": true,
"node": false,
"es2021": true
},
"extends": [
"standard",
"plugin:wc/recommended",
"plugin:lit/recommended"
],
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"ignorePatterns": ["**/*.min.js"],
"rules": {
"camelcase": 0
}
}

View File

@ -1,23 +0,0 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Defines the global state of the app
*/
export const APP_STATE = {
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
appMode: '',
// currently can be DASHBOARD or 'ROWINGGAMES'
activeRoute: 'DASHBOARD',
// contains all the rowing metrics that are delivered from the backend
metrics: {},
config: {
// currently can be FTMS, FTMSBIKE or PM5
peripheralMode: '',
// true if upload to strava is enabled
stravaUploadEnabled: false,
// true if remote device shutdown is enabled
shutdownEnabled: false
}
}

View File

@ -186,10 +186,8 @@ function getConfig () {
}
}
/*
replayRowingSession(handleRotationImpulse, {
filename: 'recordings/WRX700_2magnets.csv',
filename: 'data/recordings/2022-02-18_12-51-00_raw.csv',
realtime: true,
loop: true
})
*/

View File

@ -1,17 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": true
},
"shippedProposals": true,
"bugfixes": true
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
]
}

5941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,13 +18,12 @@
"!/**/*.test.js"
],
"scripts": {
"lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'",
"lint": "eslint ./app ./config ./app-webclient && markdownlint-cli2 '**/*.md' '#node_modules'",
"start": "node app/server.js",
"dev": "npm-run-all --parallel dev:backend dev:frontend",
"dev:backend": "nodemon --ignore 'app/client/**/*' app/server.js",
"dev:backend": "nodemon app/server.js",
"dev:frontend": "snowpack dev",
"build": "rollup -c",
"build:watch": "rollup -cw",
"build": "snowpack build",
"test": "uvu"
},
"simple-git-hooks": {
@ -37,7 +36,7 @@
"finalhandler": "1.1.2",
"form-data": "4.0.0",
"kaboom": "2001.0.0-alpha.21",
"lit": "2.1.3",
"lit": "2.2.0",
"loglevel": "1.8.0",
"nosleep.js": "0.12.0",
"onoff": "6.0.3",
@ -56,17 +55,9 @@
}
},
"devDependencies": {
"@babel/eslint-parser": "7.17.0",
"@babel/plugin-proposal-decorators": "7.17.2",
"@babel/preset-env": "7.16.11",
"@rollup/plugin-babel": "5.3.0",
"@rollup/plugin-commonjs": "21.0.1",
"@rollup/plugin-node-resolve": "13.1.3",
"@snowpack/plugin-babel": "2.1.7",
"@web/rollup-plugin-html": "1.10.1",
"axios": "0.25.0",
"eslint": "8.9.0",
"eslint-config-standard": "17.0.0-0",
"axios": "0.26.0",
"eslint": "8.10.0",
"eslint-config-standard": "17.0.0-1",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-lit": "1.6.1",
"eslint-plugin-n": "14.0.0",
@ -76,10 +67,6 @@
"markdownlint-cli2": "0.4.0",
"nodemon": "2.0.15",
"npm-run-all": "4.1.5",
"rollup": "2.67.2",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-summary": "1.3.0",
"rollup-plugin-terser": "7.0.2",
"simple-git-hooks": "2.7.0",
"snowpack": "3.8.8",
"tar": "6.1.11",

View File

@ -1,15 +1,27 @@
// Rollup bundling is currently not used any more since the experimental esbuild included in
// snowpack does seem to work just fine with Open Rowing Monitor and produces bundles of similar size
// If you want to use rollup bundling, make sure that you install the following dev dependencies
// @rollup/plugin-commonjs
// @rollup/plugin-node-resolve
// @rollup/plugin-typescript
// @web/rollup-plugin-html
// rollup
// rollup-plugin-copy
// rollup-plugin-summary
// rollup-plugin-terser
// Import rollup plugins
import { babel } from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import html from '@web/rollup-plugin-html'
import copy from 'rollup-plugin-copy'
import summary from 'rollup-plugin-summary'
import { terser } from 'rollup-plugin-terser'
import typescript from '@rollup/plugin-typescript'
// Configure an instance of @web/rollup-plugin-html
const htmlPlugin = html({
rootDir: 'app/client',
rootDir: 'app-webclient',
flattenOutput: false
})
@ -20,14 +32,15 @@ export default {
plugins: [
copy({
targets: [
{ src: 'app/client/assets/*', dest: 'build/assets' }
{ src: 'app-webclient/assets/*', dest: 'build/assets' }
]
}),
htmlPlugin,
// transpile decorators so we can use the upcoming ES decorator syntax
babel({
babelrc: true,
babelHelpers: 'bundled'
typescript({
tsconfig: './app-webclient/tsconfig.json',
compilerOptions: {
outDir: 'build/ts'
}
}),
// convert modules with commonJS syntax to ESM
commonjs(),

View File

@ -1,24 +1,22 @@
// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration
import proxy from 'http2-proxy'
import { nodeResolve } from '@rollup/plugin-node-resolve'
// import { nodeResolve } from '@rollup/plugin-node-resolve'
export default {
mount: {
// the web frontend is located in this directory
'./app/client': { url: '/' }
'./app-webclient': { url: '/' }
},
plugins: ['@snowpack/plugin-babel'],
mode: 'development',
packageOptions: {
rollup: {
plugins: [
// todo: related to the lit documentation this should enable development mode
// unfortunately this currently does not seem to work
nodeResolve({
exportConditions: ['development'],
dedupe: true
})
// nodeResolve({
// exportConditions: ['development']
// })
]
}
},
@ -29,14 +27,16 @@ export default {
buildOptions: {
out: 'build'
},
// the esbuild based bundler in snowpack is still quite young, but does seem to work
// nicely with this project
optimize: {
bundle: true,
treeshake: true,
minify: false,
minify: true,
target: 'es2020',
sourcemap: false
sourcemap: true
},
// add a proxy for websocket requests for the dev setting
// add a proxy for web socket requests for the dev setting
routes: [
{
src: '/websocket',