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,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80,
"endOfLine": "lf",
"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",
"build": "vite build",
"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": {
"core-js": "^3.6.5",
@@ -20,21 +21,21 @@
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@types/webpack": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@types/node": "^25.0.10",
"@vitejs/plugin-legacy": "^7.0.1",
"@vitejs/plugin-vue": "^6.0.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.4.2",
"sass": "^1.89.2",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"typescript": "^5.8.0",
"typescript-eslint": "^8.21.0",
"vite": "^7.0.5",
"vite-plugin-pwa": "^1.0.1",
"vue-tsc": "^2.2.0",
"workbox-build": "^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>
<style lang="scss">
@import '@/styles/app';
@use '@/styles/app';
#app {
display: flex;

View File

@@ -44,8 +44,6 @@ const check = () => {
</script>
<style lang="scss" scoped>
@import '@/styles/variables';
.dev-session {
h2 {
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(() => {
props.play ? play() : pause()
props.play ? startPlay() : stopPlay()
})
</script>

View File

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

View File

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

View File

@@ -33,8 +33,8 @@ export const useTimer = () => {
const session = computed(() => formatSeconds(sessionSeconds.value))
const intervalSeconds = computed(() => interval.value * 60)
let stopwatchId: NodeJS.Timeout | null = null
let stopWatchSessionId: NodeJS.Timeout | null = null
let stopwatchId: ReturnType<typeof setInterval> | null = null
let stopWatchSessionId: ReturnType<typeof setInterval> | null = null
watch(interval, () => {
sessionSeconds.value = interval.value * 60
@@ -55,8 +55,8 @@ export const useTimer = () => {
toggleSound.play()
notify(i18n.t('notification.change.title').toString(), {
body: i18n.t('notification.change.body', { dev }).toString()
notify(i18n.global.t('notification.change.title'), {
body: i18n.global.t('notification.change.body', { dev })
})
}
}, 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 = () => {
let wakeLock: WakeLockSentinel | null = null
@@ -22,10 +7,8 @@ export const useWakeLock = () => {
}
try {
wakeLock = await (navigator as NavigatorExtended).wakeLock.request(
'screen'
)
} catch (err) {
wakeLock = await navigator.wakeLock.request('screen')
} catch {
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() {
this.setSong(null)
this.song = null
}
},
persist: {
key: 'binome',
storage: localStorage,
paths: [
pick: [
'interval',
'hasMusic',
'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,
silent: true,
...options
})
} as NotificationOptions)
}
try {

View File

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

View File

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

View File

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

View File

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