chore: upgrade tooling stack

- TypeScript 3.9 -> 5.8
- ESLint 6 -> 9 (flat config)
- Prettier 1 -> 3
- eslint-plugin-vue 6 -> 9
- Add vue-tsc for type checking
- Migrate Sass @import to @use syntax
- Fix Pinia persistence config for v4
- Add Spotify type declarations
- Remove unused registerServiceWorker.ts
This commit is contained in:
Julien Calixte
2026-01-24 21:36:43 +01:00
parent 5a306fa741
commit 8887cdbf88
21 changed files with 857 additions and 4358 deletions

View File

@@ -1,21 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'@typescript-eslint/camelcase': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

View File

@@ -1,5 +1,9 @@
{ {
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80,
"endOfLine": "lf",
"arrowParens": "always" "arrowParens": "always"
} }

33
eslint.config.js Normal file
View File

@@ -0,0 +1,33 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import tseslint from 'typescript-eslint'
import eslintConfigPrettier from 'eslint-config-prettier'
export default tseslint.config(
{
ignores: ['dist/**', 'node_modules/**', '*.config.js', '*.config.ts']
},
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-empty-object-type': 'off',
'vue/multi-word-component-names': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
},
eslintConfigPrettier
)

View File

@@ -7,7 +7,8 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "type-check": "vue-tsc --noEmit",
"lint": "eslint src --fix"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
@@ -20,21 +21,21 @@
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2",
"@intlify/unplugin-vue-i18n": "^6.0.8", "@intlify/unplugin-vue-i18n": "^6.0.8",
"@types/webpack": "^4.4.0", "@types/node": "^25.0.10",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vitejs/plugin-legacy": "^7.0.1", "@vitejs/plugin-legacy": "^7.0.1",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"eslint": "^6.7.2", "eslint": "^9.18.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^9.32.0",
"prettier": "^1.19.1", "prettier": "^3.4.2",
"sass": "^1.89.2", "sass": "^1.89.2",
"sass-loader": "^8.0.2", "typescript": "^5.8.0",
"typescript": "~3.9.3", "typescript-eslint": "^8.21.0",
"vite": "^7.0.5", "vite": "^7.0.5",
"vite-plugin-pwa": "^1.0.1", "vite-plugin-pwa": "^1.0.1",
"vue-tsc": "^2.2.0",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0" "workbox-window": "^7.3.0"
} }

4936
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ import SWNewVersion from '@/components/SWNewVersion.vue'
</script> </script>
<style lang="scss"> <style lang="scss">
@import '@/styles/app'; @use '@/styles/app';
#app { #app {
display: flex; display: flex;

View File

@@ -44,8 +44,6 @@ const check = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/variables';
.dev-session { .dev-session {
h2 { h2 {
font-size: 24pt; font-size: 24pt;

View File

@@ -34,9 +34,9 @@ const onPause = () => {
} }
} }
const { ready, play, pause } = useSpotify(onPlay, onPause) const { ready, play: startPlay, pause: stopPlay } = useSpotify(onPlay, onPause)
watchEffect(() => { watchEffect(() => {
props.play ? play() : pause() props.play ? startPlay() : stopPlay()
}) })
</script> </script>

View File

@@ -92,8 +92,6 @@ const onPause = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/variables';
.pm-field { .pm-field {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -9,7 +9,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
export const useSpotify = (onPlay: () => void, onPause: () => void) => { export const useSpotify = (onPlay: () => void, onPause: () => void) => {
const ready = ref(false) const ready = ref(false)
const deviceId = ref<string | null>(null) const deviceId = ref<string | null>(null)
const spotify = reactive({ const spotify = reactive<{ player: SpotifyPlayer | null }>({
player: null player: null
}) })
const store = useMainStore() const store = useMainStore()
@@ -26,25 +26,29 @@ export const useSpotify = (onPlay: () => void, onPause: () => void) => {
spotify.player = new Spotify.Player({ spotify.player = new Spotify.Player({
name: 'binome', name: 'binome',
getOAuthToken: (callback) => callback(accessToken.value) getOAuthToken: (callback: (token: string) => void) =>
callback(accessToken.value ?? '')
}) })
spotify.player.addListener('ready', ({ device_id }) => { spotify.player.addListener('ready', (data: unknown) => {
const { device_id } = data as { device_id: string }
deviceId.value = device_id deviceId.value = device_id
ready.value = true ready.value = true
}) })
spotify.player.addListener('player_state_changed', (state) => { spotify.player.addListener('player_state_changed', (data: unknown) => {
if (!state) { const state = data as SpotifyPlayerState | null
return if (!state) {
} return
}
if (state.paused) { if (state.paused) {
store.setSong(null) store.song = null
onPause() onPause()
} else { } else {
store.setSong(state.track_window?.current_track?.name ?? null) store.song = state.track_window?.current_track?.name ?? null
onPlay() onPlay()
}
} }
}) )
await spotify.player.connect() await spotify.player.connect()
} }
import('@/lib/spotify-player') import('@/lib/spotify-player')
@@ -59,7 +63,9 @@ export const useSpotify = (onPlay: () => void, onPause: () => void) => {
const playerState = await spotify.player.getCurrentState() const playerState = await spotify.player.getCurrentState()
if (!playerState) { if (!playerState) {
playOnSpotify(accessToken.value, deviceId.value) if (accessToken.value && deviceId.value) {
playOnSpotify(accessToken.value, deviceId.value)
}
return return
} }
await spotify.player.resume() await spotify.player.resume()
@@ -71,7 +77,7 @@ export const useSpotify = (onPlay: () => void, onPause: () => void) => {
} }
const pause = () => { const pause = () => {
if (ready.value) { if (ready.value && accessToken.value && deviceId.value) {
pauseOnSpotify(accessToken.value, deviceId.value) pauseOnSpotify(accessToken.value, deviceId.value)
} }
} }
@@ -96,9 +102,9 @@ export const useSpotifyConnect = (hash: string) => {
return acc return acc
}, {}) }, {})
store.setAccessToken(connection['access_token']) store.accessToken = connection['access_token']
const now = new Date() const now = new Date()
store.setTokenExpire( store.tokenExpire = new Date(
new Date(now.getTime() + parseInt(connection['expires_in']) * 1000) now.getTime() + parseInt(connection['expires_in']) * 1000
) )
} }

View File

@@ -33,8 +33,8 @@ export const useTimer = () => {
const session = computed(() => formatSeconds(sessionSeconds.value)) const session = computed(() => formatSeconds(sessionSeconds.value))
const intervalSeconds = computed(() => interval.value * 60) const intervalSeconds = computed(() => interval.value * 60)
let stopwatchId: NodeJS.Timeout | null = null let stopwatchId: ReturnType<typeof setInterval> | null = null
let stopWatchSessionId: NodeJS.Timeout | null = null let stopWatchSessionId: ReturnType<typeof setInterval> | null = null
watch(interval, () => { watch(interval, () => {
sessionSeconds.value = interval.value * 60 sessionSeconds.value = interval.value * 60
@@ -55,8 +55,8 @@ export const useTimer = () => {
toggleSound.play() toggleSound.play()
notify(i18n.t('notification.change.title').toString(), { notify(i18n.global.t('notification.change.title'), {
body: i18n.t('notification.change.body', { dev }).toString() body: i18n.global.t('notification.change.body', { dev })
}) })
} }
}, 1000) }, 1000)

View File

@@ -1,18 +1,3 @@
interface WakeLock {
request(type: string): Promise<WakeLockSentinel>
}
type WakeLockType = 'screen' | null
interface WakeLockSentinel {
release(): Promise<void>
readonly WakeLocktype: WakeLockType
}
interface NavigatorExtended extends Navigator {
readonly wakeLock: WakeLock
}
export const useWakeLock = () => { export const useWakeLock = () => {
let wakeLock: WakeLockSentinel | null = null let wakeLock: WakeLockSentinel | null = null
@@ -22,10 +7,8 @@ export const useWakeLock = () => {
} }
try { try {
wakeLock = await (navigator as NavigatorExtended).wakeLock.request( wakeLock = await navigator.wakeLock.request('screen')
'screen' } catch {
)
} catch (err) {
return return
} }
} }

2
src/lib/spotify-player.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare const _default: void
export default _default

View File

@@ -1,48 +0,0 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
import { setNotificationInstance, notify } from './utils/notification'
import i18n from '@/i18n'
import { emit } from 'retrobus'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready(sw) {
sw.getNotifications().then((notifs) => {
notifs.forEach((notif) => notif.close())
})
console.log('App is being served from cache by a service worker')
setNotificationInstance((title, options?) =>
sw.showNotification(title, options)
)
},
registered() {
console.log('Service worker has been registered.')
},
cached() {
console.log('Content has been cached for offline use.')
},
updatefound() {
console.log('New content is downloading.')
},
updated(sw) {
console.log('New content is available; please refresh.')
setNotificationInstance((title, options?) =>
sw.showNotification(title, options)
)
notify(i18n.t('notification.update.title').toString(), {
body: i18n.t('notification.update.body').toString(),
tag: 'new-version'
})
emit('new-version')
},
offline() {
console.log(
'No internet connection found. App is running in offline mode.'
)
},
error(error) {
console.error('Error during service worker registration:', error)
}
})
}

View File

@@ -68,14 +68,14 @@ export const useMainStore = defineStore('main', {
}, },
cleanStore() { cleanStore() {
this.setSong(null) this.song = null
} }
}, },
persist: { persist: {
key: 'binome', key: 'binome',
storage: localStorage, storage: localStorage,
paths: [ pick: [
'interval', 'interval',
'hasMusic', 'hasMusic',
'hasSpotify', 'hasSpotify',

42
src/types/spotify.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
declare global {
interface SpotifyPlayer {
connect(): Promise<boolean>
disconnect(): void
addListener(event: string, callback: (data: unknown) => void): void
removeListener(event: string, callback?: (data: unknown) => void): void
getCurrentState(): Promise<SpotifyPlayerState | null>
resume(): Promise<void>
pause(): Promise<void>
togglePlay(): Promise<void>
seek(position_ms: number): Promise<void>
previousTrack(): Promise<void>
nextTrack(): Promise<void>
}
interface SpotifyPlayerState {
paused: boolean
track_window?: {
current_track?: {
name: string
}
}
}
interface SpotifyPlayerConstructorOptions {
name: string
getOAuthToken: (callback: (token: string) => void) => void
}
interface SpotifyNamespace {
Player: new (options: SpotifyPlayerConstructorOptions) => SpotifyPlayer
}
interface Window {
onSpotifyWebPlaybackSDKReady?: () => void
Spotify: SpotifyNamespace
}
const Spotify: SpotifyNamespace
}
export {}

View File

@@ -21,7 +21,7 @@ export const notify: ShowNotification = async (title, options?) => {
renotify: true, renotify: true,
silent: true, silent: true,
...options ...options
}) } as NotificationOptions)
} }
try { try {

View File

@@ -101,8 +101,6 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/variables';
.home { .home {
display: flex; display: flex;
flex: 1; flex: 1;

View File

@@ -6,8 +6,6 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/variables';
.privacy { .privacy {
font-family: $serif-font-family; font-family: $serif-font-family;
flex: 1; flex: 1;

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "ES2022",
"module": "esnext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "bundler",
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"importHelpers": true, "importHelpers": true,
@@ -10,16 +10,19 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"types": ["vite/client"], "types": ["vite/client", "node"],
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.vue", "src/**/*.vue",
"src/**/*.d.ts",
"tests/**/*.ts", "tests/**/*.ts",
"tests/**/*.tsx" "tests/**/*.tsx"
], ],

View File

@@ -128,7 +128,7 @@ export default defineConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: `@import "@/styles/variables.scss";` additionalData: `@use "@/styles/variables" as *;`
} }
} }
}, },