diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e019f3c..0000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ - -main.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0807290..0000000 --- a/.eslintrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "env": { "node": true }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off" - } - } \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..9c67de7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# yarn biome format --write +4bef05bf8735e1d915950fe22662fbafbe2f0b49 + +# yarn biome lint --write +94ab7ee3d8d3c09e71e2ad6b817c5be2bdfb82fc + +# yarn biome check --write +0985e650948d8b73667f929dca82bcfb73d6850c diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5a694c8..20a5196 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,6 +22,8 @@ jobs: cache: 'yarn' - name: πŸ“¦ Install dependencies run: yarn install + - name: ☒️ Biome + run: yarn biome ci - name: πŸ§ͺ Test run: yarn test - name: πŸ”¨ Build diff --git a/README.md b/README.md index 41cd1d7..460e741 100644 --- a/README.md +++ b/README.md @@ -306,16 +306,6 @@ Run `yarn version`, enter a new version number, then push to build and prepare a > [!NOTE] > You may need to run `yarn config set version-tag-prefix ""` before running `yarn version` to ensure the version tag is created correctly. -## Improve code quality with eslint (optional) -- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code. -- To use eslint with this project, make sure to install eslint from terminal: - - `npm install -g eslint` -- To use eslint to analyze this project use this command: - - `eslint main.ts` - - eslint will then create a report with suggestions for code improvement by file and line number. -- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: - - `eslint .\src\` - ## Resources - [Strava Developers - Authentication](https://developers.strava.com/docs/authentication/) diff --git a/assets/activities.json b/assets/activities.json index c29b88c..53f86c4 100644 --- a/assets/activities.json +++ b/assets/activities.json @@ -38,14 +38,8 @@ "visibility": "followers_only", "flagged": false, "gear_id": "g10510480", - "start_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], - "end_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], + "start_latlng": [51.5073518879075, 0.127758879597427], + "end_latlng": [51.5073518879075, 0.127758879597427], "average_speed": 2.557, "max_speed": 4.864, "average_cadence": 79.4, @@ -104,14 +98,8 @@ "visibility": "followers_only", "flagged": false, "gear_id": "b10137313", - "start_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], - "end_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], + "start_latlng": [51.5073518879075, 0.127758879597427], + "end_latlng": [51.5073518879075, 0.127758879597427], "average_speed": 5.924, "max_speed": 15.392, "average_temp": 14, diff --git a/assets/activity_12271989718.json b/assets/activity_12271989718.json index 3d7fb3c..3f1c0e7 100644 --- a/assets/activity_12271989718.json +++ b/assets/activity_12271989718.json @@ -38,14 +38,8 @@ "visibility": "followers_only", "flagged": false, "gear_id": "g10510480", - "start_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], - "end_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], + "start_latlng": [51.5073518879075, 0.127758879597427], + "end_latlng": [51.5073518879075, 0.127758879597427], "average_speed": 2.557, "max_speed": 4.864, "average_cadence": 79.4, @@ -123,11 +117,8 @@ "frequency_milestone": null, "trend": { "speeds": [ - 2.8536484680572296, - 2.9200434807924016, - 2.9579552941928133, - 3.0484388453469338, - 2.9144403821355738 + 2.8536484680572296, 2.9200434807924016, 2.9579552941928133, + 3.0484388453469338, 2.9144403821355738 ], "current_activity_index": 4, "min_speed": 2.5569212495610953, @@ -137,8 +128,5 @@ }, "resource_state": 2 }, - "available_zones": [ - "heartrate", - "pace" - ] + "available_zones": ["heartrate", "pace"] } diff --git a/assets/activity_12288940553.json b/assets/activity_12288940553.json index 710b498..2615cc0 100644 --- a/assets/activity_12288940553.json +++ b/assets/activity_12288940553.json @@ -38,14 +38,8 @@ "visibility": "followers_only", "flagged": false, "gear_id": "b10137313", - "start_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], - "end_latlng": [ - 51.50735188790750, - 0.127758879597427 - ], + "start_latlng": [51.5073518879075, 0.127758879597427], + "end_latlng": [51.5073518879075, 0.127758879597427], "average_speed": 5.924, "max_speed": 15.392, "average_temp": 14, @@ -114,8 +108,5 @@ "device_name": "Garmin Edge 830", "embed_token": "42db41db128afd81c5e39bd122a4acc4ca74935f", "private_note": "Hand numbness 4/10.", - "available_zones": [ - "heartrate", - "power" - ] + "available_zones": ["heartrate", "power"] } diff --git a/assets/activity_12315055573.json b/assets/activity_12315055573.json index eed978f..3283314 100644 --- a/assets/activity_12315055573.json +++ b/assets/activity_12315055573.json @@ -92,7 +92,5 @@ "hide_from_home": false, "device_name": "Garmin Forerunner 255", "embed_token": "78a470bc8b71efcf8734ae8cf1013c0d015d45d4", - "available_zones": [ - "heartrate" - ] + "available_zones": ["heartrate"] } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..dac6a61 --- /dev/null +++ b/biome.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "ignore": ["main.js", "package.json"] + }, + "organizeImports": { + "ignore": ["main.js"], + "enabled": true + }, + "linter": { + "ignore": ["main.js"], + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn", + "noControlCharactersInRegex": "warn" + }, + "style": { + "noNonNullAssertion": "warn" + }, + "complexity": { + "useLiteralKeys": "off", + "noForEach": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 71ffaca..d4ffd5d 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,49 +1,49 @@ -import esbuild from "esbuild"; -import process from "process"; +import process from "node:process"; import builtins from "builtin-modules"; +import esbuild from "esbuild"; -const banner = -`/* +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; -const prod = (process.argv[2] === "production"); +const prod = process.argv[2] === "production"; const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ["src/StravaSync.ts"], - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", - minify: prod, + banner: { + js: banner, + }, + entryPoints: ["src/StravaSync.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, }); if (prod) { - await context.rebuild(); - process.exit(0); + await context.rebuild(); + process.exit(0); } else { - await context.watch(); + await context.watch(); } diff --git a/jest.config.js b/jest.config.js index 1d936ba..8a75566 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,11 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - moduleFileExtensions: ['ts', 'js'], - testMatch: ['**/__tests__/**/*.test.ts'], + preset: "ts-jest", + testEnvironment: "node", + moduleFileExtensions: ["ts", "js"], + testMatch: ["**/__tests__/**/*.test.ts"], transform: { - '^.+\\.ts$': 'ts-jest', + "^.+\\.ts$": "ts-jest", }, - setupFilesAfterEnv: ['/jest.setup.js'], - transformIgnorePatterns: [ - 'node_modules/(?!(csv-parse)/)' - ], + setupFilesAfterEnv: ["/jest.setup.js"], + transformIgnorePatterns: ["node_modules/(?!(csv-parse)/)"], }; diff --git a/jest.setup.js b/jest.setup.js index 9967805..5e2a0a5 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,5 +1,5 @@ -jest.mock('csv-parse/browser/esm/sync', () => { +jest.mock("csv-parse/browser/esm/sync", () => { return { - parse: require('csv-parse/sync').parse + parse: require("csv-parse/sync").parse, }; }); diff --git a/manifest.json b/manifest.json index 82d15a2..140e55e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { - "id": "strava-sync", - "name": "Strava Sync", - "version": "0.2.0", - "minAppVersion": "0.15.0", - "description": "Sync activities from Strava.", - "author": "Obsidian", - "authorUrl": "https://github.com/watsonbox", - "isDesktopOnly": false + "id": "strava-sync", + "name": "Strava Sync", + "version": "0.2.0", + "minAppVersion": "0.15.0", + "description": "Sync activities from Strava.", + "author": "Obsidian", + "authorUrl": "https://github.com/watsonbox", + "isDesktopOnly": false } diff --git a/package.json b/package.json index c9f0128..3fdc50b 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,36 @@ { - "name": "obsidian-strava-sync", - "version": "0.2.0", - "description": "Sync Strava activities to Obsidian", - "main": "main.js", - "scripts": { - "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "version": "node version-bump.mjs && git add manifest.json versions.json", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" - }, - "keywords": [ - "obsidian", - "strava" - ], - "author": "Howard Wilson", - "authorUrl": "https://github.com/watsonbox", - "license": "MIT", - "devDependencies": { - "@types/jest": "^29.5.13", - "@types/luxon": "^3.4.2", - "@types/mustache": "^4.2.5", - "@types/node": "^16.11.6", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", - "esbuild": "0.17.3", - "jest": "^29.7.0", - "obsidian": "latest", - "ts-jest": "^29.2.5", - "tslib": "2.4.0", - "typescript": "4.7.4" - }, - "dependencies": { - "@types/handlebars": "^4.1.0", - "csv-parse": "^5.5.6", - "handlebars": "^4.7.8", - "luxon": "^3.5.0" - } + "name": "obsidian-strava-sync", + "version": "0.2.0", + "description": "Sync Strava activities to Obsidian", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + }, + "keywords": ["obsidian", "strava"], + "author": "Howard Wilson", + "authorUrl": "https://github.com/watsonbox", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "1.9.2", + "@types/jest": "^29.5.13", + "@types/luxon": "^3.4.2", + "@types/mustache": "^4.2.5", + "@types/node": "^16.11.6", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "jest": "^29.7.0", + "obsidian": "latest", + "ts-jest": "^29.2.5", + "tslib": "2.4.0", + "typescript": "4.7.4" + }, + "dependencies": { + "@types/handlebars": "^4.1.0", + "csv-parse": "^5.5.6", + "handlebars": "^4.7.8", + "luxon": "^3.5.0" + } } diff --git a/src/ActivitiesCSVImporter.ts b/src/ActivitiesCSVImporter.ts index fdd5673..60e0e1a 100644 --- a/src/ActivitiesCSVImporter.ts +++ b/src/ActivitiesCSVImporter.ts @@ -1,5 +1,5 @@ import { parse } from "csv-parse/browser/esm/sync"; -import { Activity } from "./Activity"; +import type { Activity } from "./Activity"; const TIME_ZONE = "UTC"; @@ -19,7 +19,7 @@ const REQUIRED_COLUMNS = [ "Elevation Gain", "Elevation Low", "Elevation High", - "Calories" + "Calories", ]; export class CSVImportError extends Error { @@ -42,45 +42,55 @@ export class ActivitiesCSVImporter { try { records = parse(this.fileContents, { columns: true, - skip_empty_lines: true + skip_empty_lines: true, }); } catch (error) { throw new CSVImportError(`Failed to parse CSV: ${error.message}`); } if (records.length === 0) { - throw new CSVImportError("The CSV file is empty or contains no valid data."); + throw new CSVImportError( + "The CSV file is empty or contains no valid data.", + ); } - const missingColumns = REQUIRED_COLUMNS.filter(col => !(col in records[0])); + const missingColumns = REQUIRED_COLUMNS.filter( + (col) => !(col in records[0]), + ); if (missingColumns.length > 0) { - throw new CSVImportError(`Missing required column(s): ${missingColumns.join(", ")}`); + throw new CSVImportError( + `Missing required column(s): ${missingColumns.join(", ")}`, + ); } return records.map((record: any): Activity => { - const startDateTimestamp = Date.parse(record["Activity Date"] + ` ${TIME_ZONE}`); + const startDateTimestamp = Date.parse( + `${record["Activity Date"]} ${TIME_ZONE}`, + ); - if (isNaN(startDateTimestamp)) { - throw new CSVImportError(`Invalid date: ${record["Activity Date"] + ` ${TIME_ZONE}`}`); + if (Number.isNaN(startDateTimestamp)) { + throw new CSVImportError( + `Invalid date: ${record["Activity Date"]} ${TIME_ZONE}`, + ); } return { - id: parseInt(record["Activity ID"]), + id: Number.parseInt(record["Activity ID"]), start_date: new Date(startDateTimestamp), name: record["Activity Name"], sport_type: record["Activity Type"], description: record["Activity Description"], private_note: record["Activity Private Note"], - elapsed_time: parseFloat(record["Elapsed Time"]), // s - moving_time: parseFloat(record["Moving Time"]), // s - distance: parseFloat(record["Distance"]), // m - max_heart_rate: parseFloat(record["Max Heart Rate"]), // bpm - max_speed: parseFloat(record["Max Speed"]), // m/s (not kph) - average_speed: parseFloat(record["Average Speed"]), // m/s (not kph) - total_elevation_gain: parseFloat(record["Elevation Gain"]), // m - elev_low: parseFloat(record["Elevation Low"]), // m - elev_high: parseFloat(record["Elevation High"]), // m - calories: parseFloat(record["Calories"]) + elapsed_time: Number.parseFloat(record["Elapsed Time"]), // s + moving_time: Number.parseFloat(record["Moving Time"]), // s + distance: Number.parseFloat(record["Distance"]), // m + max_heart_rate: Number.parseFloat(record["Max Heart Rate"]), // bpm + max_speed: Number.parseFloat(record["Max Speed"]), // m/s (not kph) + average_speed: Number.parseFloat(record["Average Speed"]), // m/s (not kph) + total_elevation_gain: Number.parseFloat(record["Elevation Gain"]), // m + elev_low: Number.parseFloat(record["Elevation Low"]), // m + elev_high: Number.parseFloat(record["Elevation High"]), // m + calories: Number.parseFloat(record["Calories"]), }; }); } diff --git a/src/Activity.ts b/src/Activity.ts index bbb9f4e..c43db52 100644 --- a/src/Activity.ts +++ b/src/Activity.ts @@ -16,5 +16,5 @@ export interface Activity { elev_high: number; calories: number; - [propName: string]: any + [propName: string]: any; } diff --git a/src/ActivityImporter.ts b/src/ActivityImporter.ts index 5ad9baf..f4fec74 100644 --- a/src/ActivityImporter.ts +++ b/src/ActivityImporter.ts @@ -1,5 +1,5 @@ -import { StravaApi } from './StravaApi'; -import { Activity } from './Activity'; +import type { Activity } from "./Activity"; +import type { StravaApi } from "./StravaApi"; // The default β€œnon-upload” rate limit allows 100 requests every 15 minutes, with up to 1,000 requests per day. export class ActivityImporter { @@ -18,7 +18,7 @@ export class ActivityImporter { try { const params: { per_page: number; after?: number } = { - per_page: this.PER_PAGE + per_page: this.PER_PAGE, }; if (this.lastActivityTimestamp) { @@ -29,15 +29,17 @@ export class ActivityImporter { const detailedActivities = await Promise.all( activities.map(async (activity: any) => { - const detailedActivity = await this.stravaApi.getActivity(activity.id); + const detailedActivity = await this.stravaApi.getActivity( + activity.id, + ); return this.mapStravaActivityToActivity(detailedActivity); - }) + }), ); return detailedActivities; } catch (error) { - console.error('Error fetching activities from Strava:', error); - throw new Error('Failed to import activities from Strava'); + console.error("Error fetching activities from Strava:", error); + throw new Error("Failed to import activities from Strava"); } } @@ -47,8 +49,8 @@ export class ActivityImporter { start_date: new Date(stravaActivity.start_date), name: stravaActivity.name, sport_type: stravaActivity.sport_type, - description: stravaActivity.description || '', - private_note: stravaActivity.private_note || '', + description: stravaActivity.description || "", + private_note: stravaActivity.private_note || "", elapsed_time: stravaActivity.elapsed_time, moving_time: stravaActivity.moving_time, distance: stravaActivity.distance, @@ -58,7 +60,7 @@ export class ActivityImporter { total_elevation_gain: stravaActivity.total_elevation_gain || 0, elev_low: stravaActivity.elev_low || 0, elev_high: stravaActivity.elev_high || 0, - calories: stravaActivity.calories || 0 + calories: stravaActivity.calories || 0, }; } } diff --git a/src/ActivityRenderer.ts b/src/ActivityRenderer.ts index 6de905f..22e08cf 100644 --- a/src/ActivityRenderer.ts +++ b/src/ActivityRenderer.ts @@ -1,7 +1,7 @@ -import { DateTime } from "luxon"; import * as Handlebars from "handlebars"; +import { DateTime } from "luxon"; import { stringifyYaml } from "obsidian"; -import { Activity } from "./Activity"; +import type { Activity } from "./Activity"; export const DEFAULT_TEMPLATE = `# {{name}} @@ -17,38 +17,50 @@ Description: {{description}} {{/if}} #Strava -` +`; export class ActivityRenderer { template: string; dateFormat: string; frontMatterProperties?: string[]; - constructor(template: string, dateFormat: string, frontMatterProperties?: string[]) { + constructor( + template: string, + dateFormat: string, + frontMatterProperties?: string[], + ) { this.template = template; this.dateFormat = dateFormat; this.frontMatterProperties = frontMatterProperties; } render(activity: Activity) { - const start_date = DateTime.fromJSDate(new Date(activity.start_date)).toFormat(this.dateFormat); + const start_date = DateTime.fromJSDate( + new Date(activity.start_date), + ).toFormat(this.dateFormat); const bodyContent = Handlebars.compile(this.template)({ ...activity, start_date: start_date, - icon: this.getActivityIcon(activity.sport_type) + icon: this.getActivityIcon(activity.sport_type), }); - return (this.frontMatterProperties ? this.renderFrontMatter(activity) : "") + bodyContent; + return ( + (this.frontMatterProperties ? this.renderFrontMatter(activity) : "") + + bodyContent + ); } renderFrontMatter(activity: Activity) { const frontMatter: { [id: string]: unknown } = { - id: activity.id + id: activity.id, }; this.frontMatterProperties?.forEach((property) => { - frontMatter[property] = property === "icon" ? this.getActivityIcon(activity.sport_type) : activity[property]; + frontMatter[property] = + property === "icon" + ? this.getActivityIcon(activity.sport_type) + : activity[property]; }); return `---\n${stringifyYaml(frontMatter)}---\n`; @@ -56,84 +68,84 @@ export class ActivityRenderer { private getActivityIcon(sportType: string): string { switch (sportType.toLowerCase()) { - case 'alpineski': - case 'backcountryski': - case 'nordicski': - case 'rollerski': - return '⛷️'; - case 'badminton': - return '🏸'; - case 'canoeing': - case 'kayaking': - return 'πŸ›Ά'; - case 'crossfit': - case 'weighttraining': - case 'workout': - return 'πŸ‹οΈ'; - case 'ebikeride': - case 'ride': - case 'gravelride': - case 'velomobile': - case 'virtualride': - return '🚴'; - case 'elliptical': - case 'stairstepper': - case 'walk': - return '🚢'; - case 'emountainbikeride': - case 'mountainbikeride': - return '🚡'; - case 'golf': - return 'β›³'; - case 'handcycle': - case 'wheelchair': - return '🦽'; - case 'highintensityintervaltraining': - return 'πŸƒ'; - case 'hike': - return 'πŸ₯Ύ'; - case 'iceskate': - return '⛸️'; - case 'inlineskate': - return 'πŸ›Ό'; - case 'kitesurf': - case 'windsurf': - case 'standuppaddling': - case 'surfing': - return 'πŸ„'; - case 'pickleball': - case 'tabletennis': - return 'πŸ“'; - case 'pilates': - case 'yoga': - return '🧘'; - case 'rockclimbing': - return 'πŸ§—'; - case 'rowing': - case 'virtualrow': - return '🚣'; - case 'run': - case 'trailrun': - case 'virtualrun': - return 'πŸƒ'; - case 'sail': - return 'β›΅'; - case 'skateboard': - return 'πŸ›Ή'; - case 'snowboard': - return 'πŸ‚'; - case 'snowshoe': - return 'πŸ₯Ύ'; - case 'soccer': - return '⚽'; - case 'squash': - case 'racquetball': - case 'tennis': - return '🎾'; - case 'swim': - return '🏊'; + case "alpineski": + case "backcountryski": + case "nordicski": + case "rollerski": + return "⛷️"; + case "badminton": + return "🏸"; + case "canoeing": + case "kayaking": + return "πŸ›Ά"; + case "crossfit": + case "weighttraining": + case "workout": + return "πŸ‹οΈ"; + case "ebikeride": + case "ride": + case "gravelride": + case "velomobile": + case "virtualride": + return "🚴"; + case "elliptical": + case "stairstepper": + case "walk": + return "🚢"; + case "emountainbikeride": + case "mountainbikeride": + return "🚡"; + case "golf": + return "β›³"; + case "handcycle": + case "wheelchair": + return "🦽"; + case "highintensityintervaltraining": + return "πŸƒ"; + case "hike": + return "πŸ₯Ύ"; + case "iceskate": + return "⛸️"; + case "inlineskate": + return "πŸ›Ό"; + case "kitesurf": + case "windsurf": + case "standuppaddling": + case "surfing": + return "πŸ„"; + case "pickleball": + case "tabletennis": + return "πŸ“"; + case "pilates": + case "yoga": + return "🧘"; + case "rockclimbing": + return "πŸ§—"; + case "rowing": + case "virtualrow": + return "🚣"; + case "run": + case "trailrun": + case "virtualrun": + return "πŸƒ"; + case "sail": + return "β›΅"; + case "skateboard": + return "πŸ›Ή"; + case "snowboard": + return "πŸ‚"; + case "snowshoe": + return "πŸ₯Ύ"; + case "soccer": + return "⚽"; + case "squash": + case "racquetball": + case "tennis": + return "🎾"; + case "swim": + return "🏊"; default: - return 'πŸ…'; + return "πŸ…"; } } } diff --git a/src/ActivitySerializer.ts b/src/ActivitySerializer.ts index ad41887..6c99749 100644 --- a/src/ActivitySerializer.ts +++ b/src/ActivitySerializer.ts @@ -1,14 +1,12 @@ -import { App, normalizePath, TFolder } from "obsidian"; -import { Settings } from "./Settings"; -import { Activity } from "./Activity"; +import { type App, TFolder, normalizePath } from "obsidian"; +import type { Activity } from "./Activity"; import { ActivityRenderer } from "./ActivityRenderer"; +import type { Settings } from "./Settings"; // On Unix-like systems / is reserved and <>:"/\|?* as well as non-printable characters \u0000-\u001F on Windows // credit: https://github.com/sindresorhus/filename-reserved-regex -const REPLACEMENT_CHAR = '-'; -// eslint-disable-next-line no-control-regex +const REPLACEMENT_CHAR = "-"; const ILLEGAL_CHAR_REGEX_FILE = /[<>:"/\\|?*\u0000-\u001F]/g; -// eslint-disable-next-line no-control-regex const ILLEGAL_CHAR_REGEX_FOLDER = /[<>:"\\|?*\u0000-\u001F]/g; export class ActivitySerializer { @@ -21,33 +19,41 @@ export class ActivitySerializer { } async serialize(activity: Activity) { - const folderName = normalizePath(new ActivityRenderer(this.settings.sync.folder, this.settings.sync.folderDateFormat).render(activity)) - .replace(ILLEGAL_CHAR_REGEX_FOLDER, REPLACEMENT_CHAR); + const folderName = normalizePath( + new ActivityRenderer( + this.settings.sync.folder, + this.settings.sync.folderDateFormat, + ).render(activity), + ).replace(ILLEGAL_CHAR_REGEX_FOLDER, REPLACEMENT_CHAR); const folder = this.app.vault.getAbstractFileByPath(folderName); if (!(folder instanceof TFolder)) { await this.app.vault.createFolder(folderName); } - const fileName = normalizePath(new ActivityRenderer(this.settings.sync.filename, this.settings.sync.filenameDateFormat).render(activity)) - .replace(ILLEGAL_CHAR_REGEX_FILE, REPLACEMENT_CHAR); + const fileName = normalizePath( + new ActivityRenderer( + this.settings.sync.filename, + this.settings.sync.filenameDateFormat, + ).render(activity), + ).replace(ILLEGAL_CHAR_REGEX_FILE, REPLACEMENT_CHAR); const filePath = `${folderName}/${fileName}.md`; const fileContent = new ActivityRenderer( this.settings.activity.template, this.settings.activity.contentDateFormat, - this.settings.activity.frontMatterProperties + this.settings.activity.frontMatterProperties, ).render(activity); try { await this.app.vault.create(filePath, fileContent); } catch (error) { - if (error.toString().includes('File already exists')) { + if (error.toString().includes("File already exists")) { return false; - } else { - throw error; } + + throw error; } return true; diff --git a/src/Settings.ts b/src/Settings.ts index 7faf53b..c694dd7 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -17,22 +17,22 @@ export const VALID_FRONT_MATTER_PROPERTIES = [ "elev_low", "elev_high", "calories", - "icon" -] + "icon", +]; export const DEFAULT_SETTINGS: Settings = { authentication: { - stravaClientId: '', - stravaClientSecret: '', + stravaClientId: "", + stravaClientSecret: "", stravaAccessToken: undefined, stravaRefreshToken: undefined, - stravaTokenExpiresAt: undefined + stravaTokenExpiresAt: undefined, }, sync: { folder: "Strava/{{start_date}}", folderDateFormat: "yyyy-MM-dd", filename: "{{id}} {{name}}", - filenameDateFormat: "yyyy-MM-dd" + filenameDateFormat: "yyyy-MM-dd", }, activity: { contentDateFormat: "yyyy-MM-dd HH:mm:ss", @@ -45,11 +45,11 @@ export const DEFAULT_SETTINGS: Settings = { "elapsed_time", "moving_time", "distance", - "icon" + "icon", ], - template: DEFAULT_TEMPLATE - } -} + template: DEFAULT_TEMPLATE, + }, +}; export interface AuthenticationSettings { stravaClientId: string; diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index 1dd0fe7..4c46168 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -1,12 +1,14 @@ -import { App, Notice, PluginSettingTab, Setting } from "obsidian"; -import StravaSync from "./StravaSync"; +import { type App, Notice, PluginSettingTab, Setting } from "obsidian"; import { DEFAULT_SETTINGS, VALID_FRONT_MATTER_PROPERTIES } from "./Settings"; +import type StravaSync from "./StravaSync"; export class SettingsTab extends PluginSettingTab { plugin: StravaSync; - readonly STRAVA_CONNECT_BUTTON_IMAGE_URL = "https://cdn.jsdelivr.net/gh/watsonbox/obsidian-strava-sync@latest/assets/btn_strava_connectwith_orange.png"; - readonly STRAVA_POWERED_BY_IMAGE_URL = "https://cdn.jsdelivr.net/gh/watsonbox/obsidian-strava-sync@latest/assets/api_logo_pwrdBy_strava_horiz_light.png"; + readonly STRAVA_CONNECT_BUTTON_IMAGE_URL = + "https://cdn.jsdelivr.net/gh/watsonbox/obsidian-strava-sync@latest/assets/btn_strava_connectwith_orange.png"; + readonly STRAVA_POWERED_BY_IMAGE_URL = + "https://cdn.jsdelivr.net/gh/watsonbox/obsidian-strava-sync@latest/assets/api_logo_pwrdBy_strava_horiz_light.png"; constructor(app: App, plugin: StravaSync) { super(app, plugin); @@ -18,102 +20,115 @@ export class SettingsTab extends PluginSettingTab { containerEl.empty(); - new Setting(containerEl).setName('Authentication').setHeading(); + new Setting(containerEl).setName("Authentication").setHeading(); new Setting(containerEl) - .setName('Strava Client ID') + .setName("Strava Client ID") .setDesc( createFragment((fragment) => { fragment.append( - 'Enter your Strava API Client ID (', - fragment.createEl('a', { - text: 'instructions', - href: 'https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#sync-configuration', + "Enter your Strava API Client ID (", + fragment.createEl("a", { + text: "instructions", + href: "https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#sync-configuration", }), - ")" - ) + ")", + ); }), ) - .addText(text => text - .setPlaceholder('Enter Client ID') - .setValue(this.plugin.settings.authentication.stravaClientId) - .onChange(async (value) => { - this.plugin.settings.authentication.stravaClientId = value; - await this.plugin.saveSettings(); - })); + .addText((text) => + text + .setPlaceholder("Enter Client ID") + .setValue(this.plugin.settings.authentication.stravaClientId) + .onChange(async (value) => { + this.plugin.settings.authentication.stravaClientId = value; + await this.plugin.saveSettings(); + }), + ); new Setting(containerEl) - .setName('Strava Client Secret') + .setName("Strava Client Secret") .setDesc( createFragment((fragment) => { fragment.append( - 'Enter your Strava API Client Secret (', - fragment.createEl('a', { - text: 'instructions', - href: 'https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#sync-configuration', + "Enter your Strava API Client Secret (", + fragment.createEl("a", { + text: "instructions", + href: "https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#sync-configuration", }), - ")" - ) + ")", + ); }), ) - .addText(text => text - .setPlaceholder('Enter Client Secret') - .setValue(this.plugin.settings.authentication.stravaClientSecret) - .onChange(async (value) => { - this.plugin.settings.authentication.stravaClientSecret = value; - await this.plugin.saveSettings(); - })); + .addText((text) => + text + .setPlaceholder("Enter Client Secret") + .setValue(this.plugin.settings.authentication.stravaClientSecret) + .onChange(async (value) => { + this.plugin.settings.authentication.stravaClientSecret = value; + await this.plugin.saveSettings(); + }), + ); new Setting(containerEl) - .setName('Authenticate with Strava') - .setDesc('Click to start the OAuth flow with Strava') - .setClass('strava-sync-authenticate') - .addButton(button => { + .setName("Authenticate with Strava") + .setDesc("Click to start the OAuth flow with Strava") + .setClass("strava-sync-authenticate") + .addButton((button) => { button.buttonEl.innerHTML = ``; - button - .onClick(() => { - if (!this.plugin.settings.authentication.stravaClientId || !this.plugin.settings.authentication.stravaClientSecret) { - new Notice('πŸ›‘ Please enter your Strava Client ID and Client Secret first.', 8000); - return; - } + button.onClick(() => { + if ( + !this.plugin.settings.authentication.stravaClientId || + !this.plugin.settings.authentication.stravaClientSecret + ) { + new Notice( + "πŸ›‘ Please enter your Strava Client ID and Client Secret first.", + 8000, + ); + return; + } - window.open(this.plugin.stravaApi.buildAuthorizeUrl(), '_blank'); - }); + window.open(this.plugin.stravaApi.buildAuthorizeUrl(), "_blank"); + }); }); if (this.plugin.settings.authentication.stravaAccessToken) { const el = containerEl.createEl("div"); el.setText("βœ…"); - containerEl.find(".strava-sync-authenticate > .setting-item-control ").prepend(el); + containerEl + .find(".strava-sync-authenticate > .setting-item-control ") + .prepend(el); } - new Setting(containerEl).setName('Sync').setHeading(); + new Setting(containerEl).setName("Sync").setHeading(); new Setting(containerEl) - .setName('Folder') + .setName("Folder") .setDesc( - 'Enter the folder where the data will be stored. {{id}}, {{name}} and {{start_date}} can be used in the folder name', + "Enter the folder where the data will be stored. {{id}}, {{name}} and {{start_date}} can be used in the folder name", ) - .addText(text => text - .setPlaceholder('Enter the folder') - .setValue(this.plugin.settings.sync.folder) - .onChange(async (value) => { - this.plugin.settings.sync.folder = value - await this.plugin.saveSettings() - })); + .addText((text) => + text + .setPlaceholder("Enter the folder") + .setValue(this.plugin.settings.sync.folder) + .onChange(async (value) => { + this.plugin.settings.sync.folder = value; + await this.plugin.saveSettings(); + }), + ); new Setting(containerEl) - .setName('Folder date format') + .setName("Folder date format") .setDesc( createFragment((fragment) => { fragment.append( - 'If date is used as part of folder name, specify the format date for use. Format ', - fragment.createEl('a', { - text: 'reference', - href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + "If date is used as part of folder name, specify the format date for use. Format ", + fragment.createEl("a", { + text: "reference", + href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", }), - "." - ) + ".", + ); }), ) .addText((text) => @@ -121,38 +136,38 @@ export class SettingsTab extends PluginSettingTab { .setPlaceholder(DEFAULT_SETTINGS.sync.folderDateFormat) .setValue(this.plugin.settings.sync.folderDateFormat) .onChange(async (value) => { - this.plugin.settings.sync.folderDateFormat = value - await this.plugin.saveSettings() + this.plugin.settings.sync.folderDateFormat = value; + await this.plugin.saveSettings(); }), - ) + ); new Setting(containerEl) - .setName('Filename') + .setName("Filename") .setDesc( - 'Enter the filename where the data will be stored. {{id}}, {{name}} and {{start_date}} can be used in the filename', + "Enter the filename where the data will be stored. {{id}}, {{name}} and {{start_date}} can be used in the filename", ) .addText((text) => text - .setPlaceholder('Enter the filename') + .setPlaceholder("Enter the filename") .setValue(this.plugin.settings.sync.filename) .onChange(async (value) => { - this.plugin.settings.sync.filename = value - await this.plugin.saveSettings() + this.plugin.settings.sync.filename = value; + await this.plugin.saveSettings(); }), - ) + ); new Setting(containerEl) - .setName('Filename date format') + .setName("Filename date format") .setDesc( createFragment((fragment) => { fragment.append( - 'If date is used as part of file name, specify the format date for use. Format ', - fragment.createEl('a', { - text: 'reference', - href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + "If date is used as part of file name, specify the format date for use. Format ", + fragment.createEl("a", { + text: "reference", + href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", }), - "." - ) + ".", + ); }), ) .addText((text) => @@ -160,76 +175,90 @@ export class SettingsTab extends PluginSettingTab { .setPlaceholder(DEFAULT_SETTINGS.sync.filenameDateFormat) .setValue(this.plugin.settings.sync.filenameDateFormat) .onChange(async (value) => { - this.plugin.settings.sync.filenameDateFormat = value - await this.plugin.saveSettings() + this.plugin.settings.sync.filenameDateFormat = value; + await this.plugin.saveSettings(); }), - ); + ); new Setting(containerEl) - .setName('Import Strava bulk export') + .setName("Import Strava bulk export") .setDesc( createFragment((fragment) => { fragment.append( - 'Import activities.csv from a Strava bulk export CSV file following ', - fragment.createEl('a', { - text: 'these instructions', - href: 'https://support.strava.com/hc/en-us/articles/216918437-Exporting-your-Data-and-Bulk-Export#h_01GG58HC4F1BGQ9PQZZVANN6WF', + "Import activities.csv from a Strava bulk export CSV file following ", + fragment.createEl("a", { + text: "these instructions", + href: "https://support.strava.com/hc/en-us/articles/216918437-Exporting-your-Data-and-Bulk-Export#h_01GG58HC4F1BGQ9PQZZVANN6WF", }), - " to generate the export." - ) + " to generate the export.", + ); }), ) - .addButton(button => button - .setButtonText('Import CSV') - .setCta() - .onClick(async () => { - try { - await this.plugin.importActivitiesFromCSV(); - new Notice('Strava bulk export import completed successfully', 4000); - } catch (error) { - console.error('Error importing Strava bulk export:', error); - new Notice('Error importing Strava bulk export. Check the console for details.', 8000); - } - })); + .addButton((button) => + button + .setButtonText("Import CSV") + .setCta() + .onClick(async () => { + try { + await this.plugin.importActivitiesFromCSV(); + new Notice( + "Strava bulk export import completed successfully", + 4000, + ); + } catch (error) { + console.error("Error importing Strava bulk export:", error); + new Notice( + "Error importing Strava bulk export. Check the console for details.", + 8000, + ); + } + }), + ); - new Setting(containerEl).setName('Activity').setHeading(); + new Setting(containerEl).setName("Activity").setHeading(); new Setting(containerEl) - .setName('Properties / Front matter') + .setName("Properties / Front matter") .setDesc( createFragment((fragment) => { fragment.append( - 'Enter the metadata to be used in your note separated by commas.' - ) + "Enter the metadata to be used in your note separated by commas.", + ); }), ) .addTextArea((text) => { text - .setPlaceholder('Enter the metadata') - .setValue(this.plugin.settings.activity.frontMatterProperties.join(',')) + .setPlaceholder("Enter the metadata") + .setValue( + this.plugin.settings.activity.frontMatterProperties.join(","), + ) .onChange(async (value) => { this.plugin.settings.activity.frontMatterProperties = value - .split(',') + .split(",") .map((v) => v.trim()) - .filter((v, i, a) => VALID_FRONT_MATTER_PROPERTIES.includes(v) && a.indexOf(v) === i) - await this.plugin.saveSettings() - }) - text.inputEl.setAttr('rows', 4) - text.inputEl.setAttr('cols', 30) - }) + .filter( + (v, i, a) => + VALID_FRONT_MATTER_PROPERTIES.includes(v) && + a.indexOf(v) === i, + ); + await this.plugin.saveSettings(); + }); + text.inputEl.setAttr("rows", 4); + text.inputEl.setAttr("cols", 30); + }); new Setting(containerEl) - .setName('Activity date format') + .setName("Activity date format") .setDesc( createFragment((fragment) => { fragment.append( - 'If date is used as part of activity content, specify the format date for use. Format ', - fragment.createEl('a', { - text: 'reference', - href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + "If date is used as part of activity content, specify the format date for use. Format ", + fragment.createEl("a", { + text: "reference", + href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", }), - "." - ) + ".", + ); }), ) .addText((text) => @@ -237,51 +266,52 @@ export class SettingsTab extends PluginSettingTab { .setPlaceholder(DEFAULT_SETTINGS.activity.contentDateFormat) .setValue(this.plugin.settings.activity.contentDateFormat) .onChange(async (value) => { - this.plugin.settings.activity.contentDateFormat = value - await this.plugin.saveSettings() + this.plugin.settings.activity.contentDateFormat = value; + await this.plugin.saveSettings(); }), - ) + ); new Setting(containerEl) - .setName('Activity template') + .setName("Activity template") .setDesc( createFragment((fragment) => { fragment.append( - 'Enter template to render activities with.', - fragment.createEl('br'), - fragment.createEl('br'), - fragment.createEl('a', { - text: 'More information', - href: 'https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#content', - }) - ) + "Enter template to render activities with.", + fragment.createEl("br"), + fragment.createEl("br"), + fragment.createEl("a", { + text: "More information", + href: "https://github.com/watsonbox/obsidian-strava-sync?tab=readme-ov-file#content", + }), + ); }), ) .addExtraButton((button) => { // Add a button to reset template button - .setIcon('reset') - .setTooltip('Reset template') + .setIcon("reset") + .setTooltip("Reset template") .onClick(async () => { - this.plugin.settings.activity.template = DEFAULT_SETTINGS.activity.template - await this.plugin.saveSettings() - this.display() - new Notice('Template reset') - }) + this.plugin.settings.activity.template = + DEFAULT_SETTINGS.activity.template; + await this.plugin.saveSettings(); + this.display(); + new Notice("Template reset"); + }); }) .addTextArea((text) => { text - .setPlaceholder('Enter the template') + .setPlaceholder("Enter the template") .setValue(this.plugin.settings.activity.template) .onChange(async (value) => { this.plugin.settings.activity.template = value ? value - : DEFAULT_SETTINGS.activity.template - await this.plugin.saveSettings() - }) - text.inputEl.setAttr('rows', 15) - text.inputEl.setAttr('cols', 50) - }) + : DEFAULT_SETTINGS.activity.template; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttr("rows", 15); + text.inputEl.setAttr("cols", 50); + }); const el = containerEl.createEl("div"); el.innerHTML = ``; diff --git a/src/StravaApi.ts b/src/StravaApi.ts index 1c01843..ac9ebda 100644 --- a/src/StravaApi.ts +++ b/src/StravaApi.ts @@ -1,8 +1,8 @@ -import { AuthenticationSettings } from "./Settings"; +import type { AuthenticationSettings } from "./Settings"; export class StravaApi { - private readonly SCOPES = 'read,activity:read_all'; - private readonly REDIRECT_URI = 'obsidian://obsidian-strava-sync'; + private readonly SCOPES = "read,activity:read_all"; + private readonly REDIRECT_URI = "obsidian://obsidian-strava-sync"; settings: AuthenticationSettings; @@ -11,16 +11,20 @@ export class StravaApi { } isAuthenticated(): boolean { - return !!(this.settings.stravaAccessToken && this.settings.stravaRefreshToken && this.settings.stravaTokenExpiresAt); + return !!( + this.settings.stravaAccessToken && + this.settings.stravaRefreshToken && + this.settings.stravaTokenExpiresAt + ); } buildAuthorizeUrl(): string { const params = new URLSearchParams({ client_id: this.settings.stravaClientId, redirect_uri: this.REDIRECT_URI, - response_type: 'code', + response_type: "code", scope: this.SCOPES, - approval_prompt: 'force' + approval_prompt: "force", }); return `https://www.strava.com/oauth/authorize?${params.toString()}`; @@ -29,15 +33,17 @@ export class StravaApi { async exchangeCodeForToken(code: string) { this.updateTokens( await this.tokenRequest({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code: code, - }) + }), ); } async refreshTokenIfExpired(): Promise { if (!this.isAuthenticated()) { - throw new Error("Not authenticated. Please authenticate with Strava first."); + throw new Error( + "Not authenticated. Please authenticate with Strava first.", + ); } if (Date.now() / 1000 > this.settings.stravaTokenExpiresAt!) { @@ -52,22 +58,22 @@ export class StravaApi { this.updateTokens( await this.tokenRequest({ - grant_type: 'refresh_token', + grant_type: "refresh_token", refresh_token: this.settings.stravaRefreshToken, - }) + }), ); } private async tokenRequest(params: Record): Promise { - const response = await fetch('https://www.strava.com/oauth/token', { - method: 'POST', + const response = await fetch("https://www.strava.com/oauth/token", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ client_id: this.settings.stravaClientId, client_secret: this.settings.stravaClientSecret, - ...params + ...params, }), }); @@ -80,25 +86,33 @@ export class StravaApi { this.settings.stravaTokenExpiresAt = response.expires_at; } - async listActivities(params: { per_page: number; after?: number }): Promise { + async listActivities(params: { per_page: number; after?: number }): Promise< + any[] + > { await this.refreshTokenIfExpired(); const queryParams = new URLSearchParams(params as any).toString(); - const response = await fetch(`https://www.strava.com/api/v3/athlete/activities?${queryParams}`, { - headers: { - 'Authorization': `Bearer ${this.settings.stravaAccessToken}` - } - }); + const response = await fetch( + `https://www.strava.com/api/v3/athlete/activities?${queryParams}`, + { + headers: { + Authorization: `Bearer ${this.settings.stravaAccessToken}`, + }, + }, + ); return response.json(); } async getActivity(id: number): Promise { await this.refreshTokenIfExpired(); - const response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, { - headers: { - 'Authorization': `Bearer ${this.settings.stravaAccessToken}` - } - }); + const response = await fetch( + `https://www.strava.com/api/v3/activities/${id}`, + { + headers: { + Authorization: `Bearer ${this.settings.stravaAccessToken}`, + }, + }, + ); return response.json(); } diff --git a/src/StravaSync.ts b/src/StravaSync.ts index 0165c2a..3b57b0e 100644 --- a/src/StravaSync.ts +++ b/src/StravaSync.ts @@ -1,150 +1,174 @@ -import { addIcon, Notice, Plugin } from 'obsidian'; -import { DEFAULT_SETTINGS, Settings } from "./Settings"; +import { Notice, Plugin, addIcon } from "obsidian"; +import { ActivitiesCSVImporter, CSVImportError } from "./ActivitiesCSVImporter"; +import type { Activity } from "./Activity"; +import { ActivityImporter } from "./ActivityImporter"; +import { ActivitySerializer } from "./ActivitySerializer"; +import { FileSelector } from "./FileSelector"; +import { DEFAULT_SETTINGS, type Settings } from "./Settings"; import { SettingsTab } from "./SettingsTab"; -import { Activity } from './Activity'; -import { ActivitiesCSVImporter, CSVImportError } from './ActivitiesCSVImporter'; -import { FileSelector } from './FileSelector'; -import { ActivitySerializer } from './ActivitySerializer'; -import { ActivityImporter } from './ActivityImporter'; -import { StravaApi } from './StravaApi'; +import { StravaApi } from "./StravaApi"; const ICON_ID = "strava"; const SUCCESS_NOTICE_DURATION = 4000; const ERROR_NOTICE_DURATION = 8000; export default class StravaSync extends Plugin { - settings: Settings; - settingsTab: SettingsTab; - stravaApi: StravaApi; - activities: Activity[] = []; - fileSelector: FileSelector; - activitySerializer: ActivitySerializer; - - async onload() { - await this.loadSettings(); - - this.settingsTab = new SettingsTab(this.app, this); - this.stravaApi = new StravaApi(this.settings.authentication); - this.fileSelector = new FileSelector(".csv"); - this.activitySerializer = new ActivitySerializer(this.app, this.settings); - - addIcon( - ICON_ID, - ``, - ) - - this.addRibbonIcon(ICON_ID, 'Import new activities from Strava', (evt: MouseEvent) => { - this.importNewActivities(); - }); - - this.registerObsidianProtocolHandler( - 'obsidian-strava-sync', - async (args) => { - await this.stravaApi.exchangeCodeForToken(args.code); - this.settings.authentication = this.stravaApi.settings; - await this.saveSettings(); - - new Notice('βœ… Successfully authenticated with Strava!', SUCCESS_NOTICE_DURATION); - - // Refresh the settings tab to update the authentication status - this.settingsTab.display(); - } - ) - - this.addCommand({ - id: 'import-new', - name: 'Import new activities from Strava', - callback: () => { - this.importNewActivities(); - } - }); - - this.addCommand({ - id: 'import-csv', - name: 'Import Strava activities from bulk export CSV', - callback: () => { - this.importActivitiesFromCSV(); - } - }); - - this.addSettingTab(this.settingsTab); - } - - onunload() { - - } - - async importActivitiesFromCSV() { - try { - const fileContents = await this.fileSelector.selectContents(); - this.activities = await new ActivitiesCSVImporter(fileContents).import(); - - await this.serializeActivities(false); - } catch (error) { - if (error instanceof CSVImportError) { - new Notice(`πŸ›‘ CSV Import Error:\n\n${error.message}`, ERROR_NOTICE_DURATION); - } else { - console.error("Unexpected error during CSV import:", error); - new Notice(`πŸ›‘ An unexpected error occurred during import. Check the console for details.`, ERROR_NOTICE_DURATION); - } - } - } - - async importNewActivities() { - try { - if (!this.stravaApi.isAuthenticated()) { - new Notice(`πŸ›‘ Please authenticate with Strava first in the plugin settings.`, ERROR_NOTICE_DURATION); - return; - } - - new Notice(`πŸ”„ Importing new activities from Strava...`, SUCCESS_NOTICE_DURATION); - - this.activities = await new ActivityImporter( - this.stravaApi, - this.settings.sync.lastActivityTimestamp - ).importLatestActivities(); - - await this.serializeActivities(true); - - if (this.activities.length > 0) { - this.settings.sync.lastActivityTimestamp = Math.max(...this.activities.map(activity => activity.start_date.getTime() / 1000)); - } - - await this.saveSettings(); - } catch (error) { - console.error("Unexpected error during Strava import:", error); - new Notice(`πŸ›‘ An unexpected error occurred during import. Check the console for details.`, ERROR_NOTICE_DURATION); - } - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - } - - private async serializeActivities(newLabel: boolean) { - let createdCount = 0; - let updatedCount = 0; - - await Promise.all( - this.activities.map(async (activity) => { - if (await this.activitySerializer.serialize(activity)) { - createdCount++; - } else { - updatedCount++; - } - }) - ); - - let message = `πŸƒ ${createdCount} ${newLabel ? 'new ' : ''}activities created`; - - if (updatedCount > 0) { - message += `, ${updatedCount} already existing`; - } - - new Notice(`${message}.`, SUCCESS_NOTICE_DURATION); - } + settings: Settings; + settingsTab: SettingsTab; + stravaApi: StravaApi; + activities: Activity[] = []; + fileSelector: FileSelector; + activitySerializer: ActivitySerializer; + + async onload() { + await this.loadSettings(); + + this.settingsTab = new SettingsTab(this.app, this); + this.stravaApi = new StravaApi(this.settings.authentication); + this.fileSelector = new FileSelector(".csv"); + this.activitySerializer = new ActivitySerializer(this.app, this.settings); + + addIcon( + ICON_ID, + ``, + ); + + this.addRibbonIcon( + ICON_ID, + "Import new activities from Strava", + (evt: MouseEvent) => { + this.importNewActivities(); + }, + ); + + this.registerObsidianProtocolHandler( + "obsidian-strava-sync", + async (args) => { + await this.stravaApi.exchangeCodeForToken(args.code); + this.settings.authentication = this.stravaApi.settings; + await this.saveSettings(); + + new Notice( + "βœ… Successfully authenticated with Strava!", + SUCCESS_NOTICE_DURATION, + ); + + // Refresh the settings tab to update the authentication status + this.settingsTab.display(); + }, + ); + + this.addCommand({ + id: "import-new", + name: "Import new activities from Strava", + callback: () => { + this.importNewActivities(); + }, + }); + + this.addCommand({ + id: "import-csv", + name: "Import Strava activities from bulk export CSV", + callback: () => { + this.importActivitiesFromCSV(); + }, + }); + + this.addSettingTab(this.settingsTab); + } + + onunload() {} + + async importActivitiesFromCSV() { + try { + const fileContents = await this.fileSelector.selectContents(); + this.activities = await new ActivitiesCSVImporter(fileContents).import(); + + await this.serializeActivities(false); + } catch (error) { + if (error instanceof CSVImportError) { + new Notice( + `πŸ›‘ CSV Import Error:\n\n${error.message}`, + ERROR_NOTICE_DURATION, + ); + } else { + console.error("Unexpected error during CSV import:", error); + new Notice( + "πŸ›‘ An unexpected error occurred during import. Check the console for details.", + ERROR_NOTICE_DURATION, + ); + } + } + } + + async importNewActivities() { + try { + if (!this.stravaApi.isAuthenticated()) { + new Notice( + "πŸ›‘ Please authenticate with Strava first in the plugin settings.", + ERROR_NOTICE_DURATION, + ); + return; + } + + new Notice( + "πŸ”„ Importing new activities from Strava...", + SUCCESS_NOTICE_DURATION, + ); + + this.activities = await new ActivityImporter( + this.stravaApi, + this.settings.sync.lastActivityTimestamp, + ).importLatestActivities(); + + await this.serializeActivities(true); + + if (this.activities.length > 0) { + this.settings.sync.lastActivityTimestamp = Math.max( + ...this.activities.map( + (activity) => activity.start_date.getTime() / 1000, + ), + ); + } + + await this.saveSettings(); + } catch (error) { + console.error("Unexpected error during Strava import:", error); + new Notice( + "πŸ›‘ An unexpected error occurred during import. Check the console for details.", + ERROR_NOTICE_DURATION, + ); + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + private async serializeActivities(newLabel: boolean) { + let createdCount = 0; + let updatedCount = 0; + + await Promise.all( + this.activities.map(async (activity) => { + if (await this.activitySerializer.serialize(activity)) { + createdCount++; + } else { + updatedCount++; + } + }), + ); + + let message = `πŸƒ ${createdCount} ${newLabel ? "new " : ""}activities created`; + + if (updatedCount > 0) { + message += `, ${updatedCount} already existing`; + } + + new Notice(`${message}.`, SUCCESS_NOTICE_DURATION); + } } diff --git a/src/__mocks__/ActivityRenderer.ts b/src/__mocks__/ActivityRenderer.ts index 51ca7cb..e0c7a37 100644 --- a/src/__mocks__/ActivityRenderer.ts +++ b/src/__mocks__/ActivityRenderer.ts @@ -1,5 +1,9 @@ export class ActivityRenderer { - constructor(public template: string, public dateFormat: string, public frontMatterProperties?: string[]) { } + constructor( + public template: string, + public dateFormat: string, + public frontMatterProperties?: string[], + ) {} render = jest.fn().mockImplementation(() => { return `Rendered ${this.template}`; diff --git a/src/__tests__/ActivityImporter.test.ts b/src/__tests__/ActivityImporter.test.ts index 31a39cd..b164193 100644 --- a/src/__tests__/ActivityImporter.test.ts +++ b/src/__tests__/ActivityImporter.test.ts @@ -1,27 +1,51 @@ -import { ActivityImporter } from '../ActivityImporter'; -import { StravaApi } from '../StravaApi'; -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { ActivityImporter } from "../ActivityImporter"; +import { StravaApi } from "../StravaApi"; -jest.mock('../StravaApi'); +jest.mock("../StravaApi"); -describe('ActivityImporter', () => { +describe("ActivityImporter", () => { let activityImporter: ActivityImporter; let mockStravaApi: jest.Mocked; beforeEach(() => { mockStravaApi = new StravaApi({} as any) as jest.Mocked; - mockStravaApi.refreshTokenIfExpired = jest.fn().mockResolvedValue(undefined); + mockStravaApi.refreshTokenIfExpired = jest + .fn() + .mockResolvedValue(undefined); activityImporter = new ActivityImporter(mockStravaApi); }); - it('should import latest activities', async () => { - const mockActivitiesList = JSON.parse(fs.readFileSync(path.join(__dirname, '../../assets/activities.json'), 'utf8')); - const mockActivity1 = JSON.parse(fs.readFileSync(path.join(__dirname, '../../assets/activity_12271989718.json'), 'utf8')); - const mockActivity2 = JSON.parse(fs.readFileSync(path.join(__dirname, '../../assets/activity_12288940553.json'), 'utf8')); - const mockActivity3 = JSON.parse(fs.readFileSync(path.join(__dirname, '../../assets/activity_12315055573.json'), 'utf8')); + it("should import latest activities", async () => { + const mockActivitiesList = JSON.parse( + fs.readFileSync( + path.join(__dirname, "../../assets/activities.json"), + "utf8", + ), + ); + const mockActivity1 = JSON.parse( + fs.readFileSync( + path.join(__dirname, "../../assets/activity_12271989718.json"), + "utf8", + ), + ); + const mockActivity2 = JSON.parse( + fs.readFileSync( + path.join(__dirname, "../../assets/activity_12288940553.json"), + "utf8", + ), + ); + const mockActivity3 = JSON.parse( + fs.readFileSync( + path.join(__dirname, "../../assets/activity_12315055573.json"), + "utf8", + ), + ); - (mockStravaApi.listActivities as jest.Mock).mockResolvedValue(mockActivitiesList); + (mockStravaApi.listActivities as jest.Mock).mockResolvedValue( + mockActivitiesList, + ); (mockStravaApi.getActivity as jest.Mock) .mockResolvedValueOnce(mockActivity1) .mockResolvedValueOnce(mockActivity2) @@ -36,27 +60,27 @@ describe('ActivityImporter', () => { expect(activities).toHaveLength(3); expect(activities[0]).toMatchObject({ id: 12271989718, - name: 'Lunch Run', - sport_type: 'Run', - start_date: new Date('2024-08-24T10:47:26Z'), + name: "Lunch Run", + sport_type: "Run", + start_date: new Date("2024-08-24T10:47:26Z"), elapsed_time: 1955, distance: 4551.3, }); expect(activities[1]).toMatchObject({ id: 12288940553, - name: 'Dynamo Challenge 2024', - sport_type: 'Ride', - start_date: new Date('2024-08-28T05:07:43Z'), + name: "Dynamo Challenge 2024", + sport_type: "Ride", + start_date: new Date("2024-08-28T05:07:43Z"), elapsed_time: 38846, distance: 154081.0, }); expect(activities[2]).toMatchObject({ id: 12315055573, - name: 'Lunch Swim', - sport_type: 'Swim', - start_date: new Date('2024-09-09T10:23:12Z'), + name: "Lunch Swim", + sport_type: "Swim", + start_date: new Date("2024-09-09T10:23:12Z"), elapsed_time: 1697, distance: 1000.0, }); diff --git a/src/__tests__/ActivityRenderer.test.ts b/src/__tests__/ActivityRenderer.test.ts index d855505..09ec53c 100644 --- a/src/__tests__/ActivityRenderer.test.ts +++ b/src/__tests__/ActivityRenderer.test.ts @@ -1,32 +1,34 @@ -import { ActivityRenderer, DEFAULT_TEMPLATE } from '../ActivityRenderer'; -import { Activity } from '../Activity'; +import type { Activity } from "../Activity"; +import { ActivityRenderer, DEFAULT_TEMPLATE } from "../ActivityRenderer"; -jest.mock('obsidian', () => ({ +jest.mock("obsidian", () => ({ stringifyYaml: jest.fn().mockImplementation((obj) => { // Simple YAML-like string representation - return Object.entries(obj).map(([key, value]) => `${key}: ${value}\n`).join(''); - }) + return Object.entries(obj) + .map(([key, value]) => `${key}: ${value}\n`) + .join(""); + }), })); -jest.mock('luxon', () => ({ +jest.mock("luxon", () => ({ DateTime: { fromJSDate: jest.fn().mockReturnValue({ - toFormat: jest.fn().mockReturnValue('2023-04-15 10:30:00') - }) - } + toFormat: jest.fn().mockReturnValue("2023-04-15 10:30:00"), + }), + }, })); -describe('ActivityRenderer', () => { +describe("ActivityRenderer", () => { let activity: Activity; beforeEach(() => { activity = { id: 123456789, - start_date: new Date('2023-04-15T10:30:00Z'), - name: 'Morning Run', - sport_type: 'Run', - description: 'Great run in the park', - private_note: 'Felt strong today', + start_date: new Date("2023-04-15T10:30:00Z"), + name: "Morning Run", + sport_type: "Run", + description: "Great run in the park", + private_note: "Felt strong today", elapsed_time: 3600, moving_time: 3500, distance: 10000, @@ -36,12 +38,15 @@ describe('ActivityRenderer', () => { total_elevation_gain: 100, elev_low: 50, elev_high: 150, - calories: 500 + calories: 500, }; }); - test('renders activity with default template', () => { - const renderer = new ActivityRenderer(DEFAULT_TEMPLATE, 'yyyy-MM-dd HH:mm:ss'); + test("renders activity with default template", () => { + const renderer = new ActivityRenderer( + DEFAULT_TEMPLATE, + "yyyy-MM-dd HH:mm:ss", + ); const result = renderer.render(activity); const expected = `# Morning Run @@ -59,7 +64,7 @@ Description: Great run in the park expect(result).toBe(expected); }); - test('renders activity with custom template', () => { + test("renders activity with custom template", () => { const customTemplate = ` Activity: {{name}} Date: {{start_date}} @@ -68,7 +73,10 @@ Distance: {{distance}} meters Time: {{elapsed_time}} seconds Description: {{description}} `; - const renderer = new ActivityRenderer(customTemplate, 'yyyy-MM-dd HH:mm:ss'); + const renderer = new ActivityRenderer( + customTemplate, + "yyyy-MM-dd HH:mm:ss", + ); const result = renderer.render(activity); const expected = ` @@ -83,8 +91,12 @@ Description: Great run in the park expect(result).toBe(expected); }); - test('renders front matter', () => { - const renderer = new ActivityRenderer(DEFAULT_TEMPLATE, 'yyyy-MM-dd HH:mm:ss', ['id', 'name', 'sport_type', 'distance']); + test("renders front matter", () => { + const renderer = new ActivityRenderer( + DEFAULT_TEMPLATE, + "yyyy-MM-dd HH:mm:ss", + ["id", "name", "sport_type", "distance"], + ); const result = renderer.render(activity); const expected = `--- @@ -108,20 +120,26 @@ Description: Great run in the park expect(result).toBe(expected); }); - test('formats date correctly', () => { - const renderer = new ActivityRenderer('{{start_date}}', 'yyyy-MM-dd HH:mm:ss'); + test("formats date correctly", () => { + const renderer = new ActivityRenderer( + "{{start_date}}", + "yyyy-MM-dd HH:mm:ss", + ); const result = renderer.render(activity); - const expected = '2023-04-15 10:30:00'; + const expected = "2023-04-15 10:30:00"; expect(result).toBe(expected); }); - test('renders activity with missing description', () => { + test("renders activity with missing description", () => { const activityWithoutDescription = { ...activity }; - activityWithoutDescription.description = ''; + activityWithoutDescription.description = ""; - const renderer = new ActivityRenderer(DEFAULT_TEMPLATE, 'yyyy-MM-dd HH:mm:ss'); + const renderer = new ActivityRenderer( + DEFAULT_TEMPLATE, + "yyyy-MM-dd HH:mm:ss", + ); const result = renderer.render(activityWithoutDescription); const expected = `# Morning Run @@ -137,11 +155,14 @@ Description: Great run in the park expect(result).toBe(expected); }); - test('renders activity with missing private note', () => { + test("renders activity with missing private note", () => { const activityWithoutPrivateNote = { ...activity }; - activityWithoutPrivateNote.private_note = ''; + activityWithoutPrivateNote.private_note = ""; - const renderer = new ActivityRenderer(DEFAULT_TEMPLATE, 'yyyy-MM-dd HH:mm:ss'); + const renderer = new ActivityRenderer( + DEFAULT_TEMPLATE, + "yyyy-MM-dd HH:mm:ss", + ); const result = renderer.render(activityWithoutPrivateNote); const expected = `# Morning Run @@ -156,12 +177,16 @@ Description: Great run in the park expect(result).toBe(expected); }); - test('renders activity icon in frontmatter and content', () => { + test("renders activity icon in frontmatter and content", () => { const customTemplate = ` # {{icon}} {{name}} Sport Type: {{sport_type}} `; - const renderer = new ActivityRenderer(customTemplate, 'yyyy-MM-dd HH:mm:ss', ['icon']); + const renderer = new ActivityRenderer( + customTemplate, + "yyyy-MM-dd HH:mm:ss", + ["icon"], + ); const result = renderer.render(activity); const expected = `--- @@ -176,22 +201,22 @@ Sport Type: Run expect(result).toBe(expected); }); - test('renders correct icons for different sport types', () => { - const template = '{{icon}} {{sport_type}}'; - const renderer = new ActivityRenderer(template, 'yyyy-MM-dd HH:mm:ss'); + test("renders correct icons for different sport types", () => { + const template = "{{icon}} {{sport_type}}"; + const renderer = new ActivityRenderer(template, "yyyy-MM-dd HH:mm:ss"); const testCases = [ - { sport_type: 'Run', expectedIcon: 'πŸƒ' }, - { sport_type: 'Ride', expectedIcon: '🚴' }, - { sport_type: 'Swim', expectedIcon: '🏊' }, - { sport_type: 'AlpineSki', expectedIcon: '⛷️' }, - { sport_type: 'Golf', expectedIcon: 'β›³' }, - { sport_type: 'Hike', expectedIcon: 'πŸ₯Ύ' }, - { sport_type: 'Walk', expectedIcon: '🚢' }, - { sport_type: 'Snowboard', expectedIcon: 'πŸ‚' }, - { sport_type: 'Workout', expectedIcon: 'πŸ‹οΈ' }, - { sport_type: 'Yoga', expectedIcon: '🧘' }, - { sport_type: 'UnknownActivity', expectedIcon: 'πŸ…' }, + { sport_type: "Run", expectedIcon: "πŸƒ" }, + { sport_type: "Ride", expectedIcon: "🚴" }, + { sport_type: "Swim", expectedIcon: "🏊" }, + { sport_type: "AlpineSki", expectedIcon: "⛷️" }, + { sport_type: "Golf", expectedIcon: "β›³" }, + { sport_type: "Hike", expectedIcon: "πŸ₯Ύ" }, + { sport_type: "Walk", expectedIcon: "🚢" }, + { sport_type: "Snowboard", expectedIcon: "πŸ‚" }, + { sport_type: "Workout", expectedIcon: "πŸ‹οΈ" }, + { sport_type: "Yoga", expectedIcon: "🧘" }, + { sport_type: "UnknownActivity", expectedIcon: "πŸ…" }, ]; testCases.forEach(({ sport_type, expectedIcon }) => { diff --git a/src/__tests__/ActivitySerializer.test.ts b/src/__tests__/ActivitySerializer.test.ts index 9e489f9..9bb139f 100644 --- a/src/__tests__/ActivitySerializer.test.ts +++ b/src/__tests__/ActivitySerializer.test.ts @@ -1,12 +1,12 @@ -import { App, Vault, TFolder, normalizePath } from 'obsidian'; -import { ActivitySerializer } from '../ActivitySerializer'; -import { Settings } from '../Settings'; -import { Activity } from '../Activity'; +import { App, TFolder, Vault, normalizePath } from "obsidian"; +import type { Activity } from "../Activity"; +import { ActivitySerializer } from "../ActivitySerializer"; +import type { Settings } from "../Settings"; -jest.mock('obsidian'); -jest.mock('../ActivityRenderer'); +jest.mock("obsidian"); +jest.mock("../ActivityRenderer"); -describe('ActivitySerializer', () => { +describe("ActivitySerializer", () => { let app: App; let vault: Vault; let settings: Settings; @@ -20,34 +20,34 @@ describe('ActivitySerializer', () => { settings = { authentication: { - stravaClientId: '', - stravaClientSecret: '', + stravaClientId: "", + stravaClientSecret: "", stravaAccessToken: undefined, stravaRefreshToken: undefined, - stravaTokenExpiresAt: undefined + stravaTokenExpiresAt: undefined, }, sync: { - folder: 'Strava/{{start_date}}', - folderDateFormat: 'yyyy-MM-dd', - filename: '{{id}} {{name}}', - filenameDateFormat: 'yyyy-MM-dd' + folder: "Strava/{{start_date}}", + folderDateFormat: "yyyy-MM-dd", + filename: "{{id}} {{name}}", + filenameDateFormat: "yyyy-MM-dd", }, activity: { - contentDateFormat: 'yyyy-MM-dd HH:mm:ss', + contentDateFormat: "yyyy-MM-dd HH:mm:ss", frontMatterProperties: [], - template: '# {{name}}' - } + template: "# {{name}}", + }, }; activitySerializer = new ActivitySerializer(app, settings); testActivity = { id: 123, - start_date: new Date('2023-10-01T10:00:00Z'), - name: 'Morning Run', - sport_type: 'Run', - description: 'A nice morning run.', - private_note: 'Felt great!', + start_date: new Date("2023-10-01T10:00:00Z"), + name: "Morning Run", + sport_type: "Run", + description: "A nice morning run.", + private_note: "Felt great!", elapsed_time: 3600, moving_time: 3500, distance: 10000, @@ -61,47 +61,59 @@ describe('ActivitySerializer', () => { }; }); - test('should create a new folder and file for the activity', async () => { - const expectedFolderName = 'Rendered Strava/{{start_date}}'; - const expectedFileName = 'Rendered {{id}} {{name}}'; + test("should create a new folder and file for the activity", async () => { + const expectedFolderName = "Rendered Strava/{{start_date}}"; + const expectedFileName = "Rendered {{id}} {{name}}"; const expectedFilePath = `${expectedFolderName}/${expectedFileName}.md`; - const expectedFileContent = 'Rendered # {{name}}'; + const expectedFileContent = "Rendered # {{name}}"; (vault.getAbstractFileByPath as jest.Mock).mockReturnValue(null); (normalizePath as jest.Mock).mockImplementation((path) => path); const result = await activitySerializer.serialize(testActivity); - expect(vault.getAbstractFileByPath).toHaveBeenCalledWith(expectedFolderName); + expect(vault.getAbstractFileByPath).toHaveBeenCalledWith( + expectedFolderName, + ); expect(vault.createFolder).toHaveBeenCalledWith(expectedFolderName); - expect(vault.create).toHaveBeenCalledWith(expectedFilePath, expectedFileContent); + expect(vault.create).toHaveBeenCalledWith( + expectedFilePath, + expectedFileContent, + ); expect(result).toBe(true); }); - test('should not create a new file if it already exists', async () => { - const expectedFolderName = 'Rendered Strava/{{start_date}}'; - const expectedFileName = 'Rendered {{id}} {{name}}'; + test("should not create a new file if it already exists", async () => { + const expectedFolderName = "Rendered Strava/{{start_date}}"; + const expectedFileName = "Rendered {{id}} {{name}}"; const expectedFilePath = `${expectedFolderName}/${expectedFileName}.md`; (vault.getAbstractFileByPath as jest.Mock).mockReturnValue(new TFolder()); (normalizePath as jest.Mock).mockImplementation((path) => path); - (vault.create as jest.Mock).mockRejectedValue(new Error('File already exists')); + (vault.create as jest.Mock).mockRejectedValue( + new Error("File already exists"), + ); const result = await activitySerializer.serialize(testActivity); - expect(vault.getAbstractFileByPath).toHaveBeenCalledWith(expectedFolderName); + expect(vault.getAbstractFileByPath).toHaveBeenCalledWith( + expectedFolderName, + ); expect(vault.createFolder).not.toHaveBeenCalled(); - expect(vault.create).toHaveBeenCalledWith(expectedFilePath, expect.any(String)); + expect(vault.create).toHaveBeenCalledWith( + expectedFilePath, + expect.any(String), + ); expect(result).toBe(false); }); - test('should replace illegal characters in folder and file names', async () => { - settings.sync.folder = 'Strava/<{{start_date}}>'; - settings.sync.filename = '{{id}} {{name}}?'; + test("should replace illegal characters in folder and file names", async () => { + settings.sync.folder = "Strava/<{{start_date}}>"; + settings.sync.filename = "{{id}} {{name}}?"; activitySerializer = new ActivitySerializer(app, settings); - const expectedFolderName = 'Rendered Strava/-{{start_date}}-'; - const expectedFileName = 'Rendered {{id}} {{name}}-'; + const expectedFolderName = "Rendered Strava/-{{start_date}}-"; + const expectedFileName = "Rendered {{id}} {{name}}-"; const expectedFilePath = `${expectedFolderName}/${expectedFileName}.md`; (vault.getAbstractFileByPath as jest.Mock).mockReturnValue(null); @@ -109,9 +121,14 @@ describe('ActivitySerializer', () => { const result = await activitySerializer.serialize(testActivity); - expect(vault.getAbstractFileByPath).toHaveBeenCalledWith(expectedFolderName); + expect(vault.getAbstractFileByPath).toHaveBeenCalledWith( + expectedFolderName, + ); expect(vault.createFolder).toHaveBeenCalledWith(expectedFolderName); - expect(vault.create).toHaveBeenCalledWith(expectedFilePath, expect.any(String)); + expect(vault.create).toHaveBeenCalledWith( + expectedFilePath, + expect.any(String), + ); expect(result).toBe(true); }); }); diff --git a/src/__tests__/StravaSync.test.ts b/src/__tests__/StravaSync.test.ts index 2a343e5..22f3df6 100644 --- a/src/__tests__/StravaSync.test.ts +++ b/src/__tests__/StravaSync.test.ts @@ -1,28 +1,28 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from "node:fs"; +import * as path from "node:path"; -import { App, Vault, PluginManifest, Notice } from 'obsidian'; -import StravaSync from '../StravaSync'; +import { App, Notice, type PluginManifest, Vault } from "obsidian"; +import StravaSync from "../StravaSync"; -jest.mock('obsidian'); +jest.mock("obsidian"); -jest.mock('../ActivityImporter', () => ({ +jest.mock("../ActivityImporter", () => ({ ActivityImporter: jest.fn().mockImplementation(() => ({ importLatestActivities: jest.fn().mockResolvedValue([ { id: 12345678, - name: 'Morning Run', - type: 'Run', - start_date: new Date('2023-04-16T08:00:00Z'), + name: "Morning Run", + type: "Run", + start_date: new Date("2023-04-16T08:00:00Z"), elapsed_time: 1800, distance: 5000, max_heart_rate: 175, }, { id: 12345679, - name: 'Evening Ride', - type: 'Ride', - start_date: new Date('2023-04-16T18:00:00Z'), + name: "Evening Ride", + type: "Ride", + start_date: new Date("2023-04-16T18:00:00Z"), elapsed_time: 3600, distance: 20000, max_heart_rate: 165, @@ -31,28 +31,33 @@ jest.mock('../ActivityImporter', () => ({ })), })); -jest.mock('../FileSelector', () => ({ +jest.mock("../FileSelector", () => ({ FileSelector: jest.fn().mockImplementation(() => ({ - selectContents: jest.fn().mockResolvedValue( - fs.readFileSync(path.join(__dirname, '../../assets/activities.csv'), 'utf8') - ), + selectContents: jest + .fn() + .mockResolvedValue( + fs.readFileSync( + path.join(__dirname, "../../assets/activities.csv"), + "utf8", + ), + ), })), })); -jest.mock('../ActivitySerializer', () => ({ +jest.mock("../ActivitySerializer", () => ({ ActivitySerializer: jest.fn().mockImplementation(() => ({ serialize: jest.fn().mockResolvedValue(true), })), })); -jest.mock('../StravaApi', () => ({ +jest.mock("../StravaApi", () => ({ StravaApi: jest.fn().mockImplementation(() => ({ isAuthenticated: jest.fn().mockReturnValue(true), listActivities: jest.fn().mockResolvedValue([]), })), })); -describe('StravaSync', () => { +describe("StravaSync", () => { let plugin: StravaSync; let app: App; let vault: Vault; @@ -64,13 +69,13 @@ describe('StravaSync', () => { (app as any).vault = vault; mockManifest = { - id: 'strava-sync', - name: 'Strava Sync', - version: '1.0.0', - minAppVersion: '0.15.0', - description: 'Sync Strava activities to Obsidian', - author: 'Your Name', - authorUrl: 'https://github.com/yourusername', + id: "strava-sync", + name: "Strava Sync", + version: "1.0.0", + minAppVersion: "0.15.0", + description: "Sync Strava activities to Obsidian", + author: "Your Name", + authorUrl: "https://github.com/yourusername", isDesktopOnly: false, }; @@ -80,40 +85,42 @@ describe('StravaSync', () => { plugin.onload(); }); - test('Import activities CSV', async () => { + test("Import activities CSV", async () => { await plugin.importActivitiesFromCSV(); expect(plugin.fileSelector.selectContents).toHaveBeenCalledTimes(1); expect(plugin.activitySerializer.serialize).toHaveBeenCalledTimes(3); - const serializedActivities = (plugin.activitySerializer.serialize as jest.Mock).mock.calls.map(call => call[0]); + const serializedActivities = ( + plugin.activitySerializer.serialize as jest.Mock + ).mock.calls.map((call) => call[0]); expect(serializedActivities[0]).toMatchObject({ id: 12271989718, - name: 'Lunch Run', - sport_type: 'Run', + name: "Lunch Run", + sport_type: "Run", start_date: expect.any(Date), elapsed_time: 1955, distance: 4551.31982421875, max_heart_rate: 165, - private_note: 'Light run. Knee pain 2/10.', + private_note: "Light run. Knee pain 2/10.", }); expect(serializedActivities[1]).toMatchObject({ id: 12288940553, - name: 'Dynamo Challenge 2024', - sport_type: 'Ride', + name: "Dynamo Challenge 2024", + sport_type: "Ride", start_date: expect.any(Date), elapsed_time: 23198, distance: 93131.53125, max_heart_rate: 182, - private_note: 'Hand numbness 4/10.', + private_note: "Hand numbness 4/10.", }); expect(serializedActivities[2]).toMatchObject({ id: 12315055573, - name: 'Lunch Swim', - sport_type: 'Swim', + name: "Lunch Swim", + sport_type: "Swim", start_date: expect.any(Date), elapsed_time: 1697, distance: 1000, @@ -121,25 +128,29 @@ describe('StravaSync', () => { }); expect(Notice).toHaveBeenCalledWith( - 'πŸƒ 3 activities created.', - expect.any(Number) + "πŸƒ 3 activities created.", + expect.any(Number), ); }); - test('Import new activities', async () => { - jest.spyOn(plugin.activitySerializer, 'serialize').mockImplementation((activity) => { - return Promise.resolve(activity.id === 12345678); - }); + test("Import new activities", async () => { + jest + .spyOn(plugin.activitySerializer, "serialize") + .mockImplementation((activity) => { + return Promise.resolve(activity.id === 12345678); + }); await plugin.importNewActivities(); expect(plugin.stravaApi.isAuthenticated).toHaveBeenCalled(); expect(plugin.activitySerializer.serialize).toHaveBeenCalledTimes(2); - expect(plugin.settings.sync.lastActivityTimestamp).toBe(Math.floor(new Date('2023-04-16T18:00:00Z').getTime() / 1000)); + expect(plugin.settings.sync.lastActivityTimestamp).toBe( + Math.floor(new Date("2023-04-16T18:00:00Z").getTime() / 1000), + ); expect(Notice).toHaveBeenCalledWith( - 'πŸƒ 1 new activities created, 1 already existing.', - expect.any(Number) + "πŸƒ 1 new activities created, 1 already existing.", + expect.any(Number), ); }); }); diff --git a/tsconfig.json b/tsconfig.json index c44b729..a4108b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,14 +11,7 @@ "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, - "lib": [ - "DOM", - "ES5", - "ES6", - "ES7" - ] + "lib": ["DOM", "ES5", "ES6", "ES7"] }, - "include": [ - "**/*.ts" - ] + "include": ["**/*.ts"] } diff --git a/version-bump.mjs b/version-bump.mjs index d409fa0..9891221 100644 --- a/version-bump.mjs +++ b/version-bump.mjs @@ -1,14 +1,14 @@ -import { readFileSync, writeFileSync } from "fs"; +import { readFileSync, writeFileSync } from "node:fs"; const targetVersion = process.env.npm_package_version; // read minAppVersion from manifest.json and bump version to target version -let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); const { minAppVersion } = manifest; manifest.version = targetVersion; -writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); +writeFileSync("manifest.json", `${JSON.stringify(manifest, null, 2)}\n`); // update versions.json with target version and minAppVersion from manifest.json -let versions = JSON.parse(readFileSync("versions.json", "utf8")); +const versions = JSON.parse(readFileSync("versions.json", "utf8")); versions[targetVersion] = minAppVersion; -writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); +writeFileSync("versions.json", `${JSON.stringify(versions, null, 2)}\n`); diff --git a/versions.json b/versions.json index 7c474e7..45f974e 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "0.1.0": "0.15.0", - "0.1.1": "0.15.0", - "0.2.0": "0.15.0" -} \ No newline at end of file + "0.1.0": "0.15.0", + "0.1.1": "0.15.0", + "0.2.0": "0.15.0" +} diff --git a/yarn.lock b/yarn.lock index ea80536..8104576 100644 --- a/yarn.lock +++ b/yarn.lock @@ -291,6 +291,60 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@biomejs/biome@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.2.tgz#297a6a0172e46124b9b6058ec3875091d352a0be" + integrity sha512-4j2Gfwft8Jqp1X0qLYvK4TEy4xhTo4o6rlvJPsjPeEame8gsmbGQfOPBkw7ur+7/Z/f0HZmCZKqbMvR7vTXQYQ== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "1.9.2" + "@biomejs/cli-darwin-x64" "1.9.2" + "@biomejs/cli-linux-arm64" "1.9.2" + "@biomejs/cli-linux-arm64-musl" "1.9.2" + "@biomejs/cli-linux-x64" "1.9.2" + "@biomejs/cli-linux-x64-musl" "1.9.2" + "@biomejs/cli-win32-arm64" "1.9.2" + "@biomejs/cli-win32-x64" "1.9.2" + +"@biomejs/cli-darwin-arm64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.2.tgz#9303e426156189b2ab469154f7cd94ecfbf8bd5e" + integrity sha512-rbs9uJHFmhqB3Td0Ro+1wmeZOHhAPTL3WHr8NtaVczUmDhXkRDWScaxicG9+vhSLj1iLrW47itiK6xiIJy6vaA== + +"@biomejs/cli-darwin-x64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.2.tgz#a674e61c04b5aeca31b5b1e7f7b678937a6e90ac" + integrity sha512-BlfULKijNaMigQ9GH9fqJVt+3JTDOSiZeWOQtG/1S1sa8Lp046JHG3wRJVOvekTPL9q/CNFW1NVG8J0JN+L1OA== + +"@biomejs/cli-linux-arm64-musl@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.2.tgz#b126b0093a7b7632c01a948bf7a0feaf5040ea76" + integrity sha512-ZATvbUWhNxegSALUnCKWqetTZqrK72r2RsFD19OK5jXDj/7o1hzI1KzDNG78LloZxftrwr3uI9SqCLh06shSZw== + +"@biomejs/cli-linux-arm64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.2.tgz#c71b4aa374fcd32d3f1a9e7c8a6ce3090e9b6be4" + integrity sha512-T8TJuSxuBDeQCQzxZu2o3OU4eyLumTofhCxxFd3+aH2AEWVMnH7Z/c3QP1lHI5RRMBP9xIJeMORqDQ5j+gVZzw== + +"@biomejs/cli-linux-x64-musl@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.2.tgz#5fce02295b435362aa75044ddc388547fbbbbb19" + integrity sha512-CjPM6jT1miV5pry9C7qv8YJk0FIZvZd86QRD3atvDgfgeh9WQU0k2Aoo0xUcPdTnoz0WNwRtDicHxwik63MmSg== + +"@biomejs/cli-linux-x64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.2.tgz#17d7bac0b6ebb20b9fb18812d4ef390f5855858f" + integrity sha512-T0cPk3C3Jr2pVlsuQVTBqk2qPjTm8cYcTD9p/wmR9MeVqui1C/xTVfOIwd3miRODFMrJaVQ8MYSXnVIhV9jTjg== + +"@biomejs/cli-win32-arm64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.2.tgz#725734c4dc35cdcad3ef5cbcd85c6ff1238afa46" + integrity sha512-2x7gSty75bNIeD23ZRPXyox6Z/V0M71ObeJtvQBhi1fgrvPdtkEuw7/0wEHg6buNCubzOFuN9WYJm6FKoUHfhg== + +"@biomejs/cli-win32-x64@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.2.tgz#6f506d604d3349c1ba674d46cb96540c00a2ba83" + integrity sha512-JC3XvdYcjmu1FmAehVwVV0SebLpeNTnO2ZaMdGCSOdS7f8O9Fq14T2P1gTG1Q29Q8Dt1S03hh0IdVpIZykOL8g== + "@esbuild/android-arm64@0.17.3": version "0.17.3" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz#35d045f69c9b4cf3f8efcd1ced24a560213d3346" @@ -641,27 +695,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -767,11 +800,6 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - "@types/luxon@^3.4.2": version "3.4.2" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" @@ -818,86 +846,6 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz#c67794d2b0fd0b4a47f50266088acdc52a08aab6" - integrity sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w== - dependencies: - "@typescript-eslint/scope-manager" "5.29.0" - "@typescript-eslint/type-utils" "5.29.0" - "@typescript-eslint/utils" "5.29.0" - debug "^4.3.4" - functional-red-black-tree "^1.0.1" - ignore "^5.2.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.29.0.tgz#41314b195b34d44ff38220caa55f3f93cfca43cf" - integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw== - dependencies: - "@typescript-eslint/scope-manager" "5.29.0" - "@typescript-eslint/types" "5.29.0" - "@typescript-eslint/typescript-estree" "5.29.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz#2a6a32e3416cb133e9af8dcf54bf077a916aeed3" - integrity sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA== - dependencies: - "@typescript-eslint/types" "5.29.0" - "@typescript-eslint/visitor-keys" "5.29.0" - -"@typescript-eslint/type-utils@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz#241918001d164044020b37d26d5b9f4e37cc3d5d" - integrity sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg== - dependencies: - "@typescript-eslint/utils" "5.29.0" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.29.0.tgz#7861d3d288c031703b2d97bc113696b4d8c19aab" - integrity sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg== - -"@typescript-eslint/typescript-estree@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz#e83d19aa7fd2e74616aab2f25dfbe4de4f0b5577" - integrity sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ== - dependencies: - "@typescript-eslint/types" "5.29.0" - "@typescript-eslint/visitor-keys" "5.29.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.29.0.tgz#775046effd5019667bd086bcf326acbe32cd0082" - integrity sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.29.0" - "@typescript-eslint/types" "5.29.0" - "@typescript-eslint/typescript-estree" "5.29.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/visitor-keys@5.29.0": - version "5.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz#7a4749fa7ef5160c44a451bf060ac1dc6dfb77ee" - integrity sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ== - dependencies: - "@typescript-eslint/types" "5.29.0" - eslint-visitor-keys "^3.3.0" - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -944,11 +892,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" @@ -1210,7 +1153,7 @@ csv-parse@^5.5.6: resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a" integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -1237,13 +1180,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - ejs@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -1316,53 +1252,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -1394,29 +1288,11 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -1461,11 +1337,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -1486,13 +1357,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1510,18 +1374,6 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -1566,11 +1418,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - import-local@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" @@ -1609,11 +1456,6 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1624,13 +1466,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -2158,11 +1993,6 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - micromatch@^4.0.4: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -2315,11 +2145,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - picocolors@^1.0.0, picocolors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" @@ -2364,21 +2189,11 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -2410,24 +2225,12 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: +semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -2595,18 +2398,6 @@ tslib@2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"