├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── InternalCounts.ts ├── __tests__ │ └── markdownHelpers.test.ts ├── achievements-view │ ├── AchievementsView.svelte │ └── AchievementsView.ts ├── array-utils │ └── sort.ts ├── commands.ts ├── main.ts ├── markdownHelpers.ts ├── obsidian.d.ts ├── reset-progress-modal │ ├── ResetProgressModal.svelte │ └── ResetProgressModal.ts ├── seededAchievements.ts ├── settings-tab │ ├── SettingsTab.svelte │ └── SettingsTab.ts └── settings.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | env: { node: true }, 5 | plugins: ["@typescript-eslint", "svelte3"], 6 | overrides: [ 7 | { 8 | files: ["*.svelte"], 9 | processor: "svelte3/svelte3", 10 | }, 11 | ], 12 | extends: [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | ], 17 | parserOptions: { 18 | sourceType: "module", 19 | }, 20 | rules: { 21 | "no-unused-vars": "off", 22 | "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], 23 | "@typescript-eslint/ban-ts-comment": "off", 24 | "no-prototype-builtins": "off", 25 | "@typescript-eslint/no-empty-function": "off", 26 | }, 27 | settings: { 28 | "svelte3/typescript": () => require("typescript"), // pass the TypeScript package to the Svelte plugin 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zachatoo # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | ko_fi: zachatoo # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian Plugin 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 13 | - name: Use Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: "14.x" 17 | # Get the version number and put it in a variable 18 | - name: Get Version 19 | id: version 20 | run: | 21 | echo "::set-output name=tag::$(git describe --abbrev=0)" 22 | # Build the plugin 23 | - name: Build 24 | id: build 25 | run: | 26 | npm install 27 | npm run build --if-present 28 | # Package the required files into a zip 29 | - name: Package 30 | run: | 31 | mkdir ${{ github.event.repository.name }} 32 | cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }} 33 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} 34 | # Create the release on github 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | VERSION: ${{ github.ref }} 41 | with: 42 | tag_name: ${{ github.ref }} 43 | release_name: ${{ github.ref }} 44 | draft: false 45 | prerelease: false 46 | # Upload the packaged release file 47 | - name: Upload zip file 48 | id: upload-zip 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./${{ github.event.repository.name }}.zip 55 | asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip 56 | asset_content_type: application/zip 57 | # Upload the main.js 58 | - name: Upload main.js 59 | id: upload-main 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ steps.create_release.outputs.upload_url }} 65 | asset_path: ./main.js 66 | asset_name: main.js 67 | asset_content_type: text/javascript 68 | # Upload the manifest.json 69 | - name: Upload manifest.json 70 | id: upload-manifest 71 | uses: actions/upload-release-asset@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ steps.create_release.outputs.upload_url }} 76 | asset_path: ./manifest.json 77 | asset_name: manifest.json 78 | asset_content_type: application/json 79 | # Upload the style.css 80 | - name: Upload styles.css 81 | id: upload-css 82 | uses: actions/upload-release-asset@v1 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | with: 86 | upload_url: ${{ steps.create_release.outputs.upload_url }} 87 | asset_path: ./styles.css 88 | asset_name: styles.css 89 | asset_content_type: text/css= 90 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | 24 | - name: Install dependencies 25 | run: npm i 26 | 27 | - name: Lint 28 | run: npm run lint 29 | 30 | typecheck: 31 | name: Type Check 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup node 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 16 41 | 42 | - name: Install dependencies 43 | run: npm i 44 | 45 | - name: Type check 46 | run: npm run typecheck 47 | 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v3 54 | 55 | - name: Setup node 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version: 16 59 | 60 | - name: Install dependencies 61 | run: npm i 62 | 63 | - name: Test 64 | run: npm run test:ci 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run typecheck 5 | npm run lint -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zachary Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22obsidian-achievements%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) 2 | 3 | # Obsidian Achievements 4 | 5 | Gamify your note taking by working towards gaining achievements! 6 | 7 | ## Achievements 8 | 9 | ### Your first note 10 | 11 | Create a note. 12 | 13 | ### Note taker 14 | 15 | Create ten notes. 16 | 17 | ### Wordsmith 18 | 19 | Create one hundred notes. 20 | 21 | ### Storyteller 22 | 23 | Create one thousand notes. 24 | 25 | ### Taking out the trash 26 | 27 | Delete ten notes. 28 | 29 | ### Linking your thinking 30 | 31 | Create an internal link. You can type [[ to begin creating an internal link. 32 | 33 | ### Making connections 34 | 35 | Create ten internal links. 36 | 37 | ### Conspiracy theorist 38 | 39 | Create one hundred internal links. 40 | 41 | ### Air traffic controller 42 | 43 | Create one thousand internal links. 44 | 45 | ### Commander 46 | 47 | Open the command palette. You can find the hotkey to open the command palette in Settings - Hotkeys. 48 | 49 | ### Quickly now 50 | 51 | Open the quick switcher. You can find the hotkey to open the quick switcher in Settings - Hotkeys. 52 | 53 | ### Callouts 54 | 55 | Create a callout. You can find the hotkey to create a callout in Settings - Hotkeys. 56 | 57 | ### Headings 58 | 59 | Create a heading. You can create a heading by adding a new line to a note and typing # Heading. 60 | 61 | ### Nested headings 62 | 63 | Create at least three levels of headings in a single note. 64 | 65 | ### Your first tag 66 | 67 | Create a tag. You can create a tag by typing #tag. 68 | 69 | ### Tagging apprentice 70 | 71 | Create five unique tags. 72 | 73 | ### Tagging expert 74 | 75 | Create ten unique tags. 76 | 77 | ## Notice 78 | 79 | There's some known issues on mobile with achievements being achieved much quicker than expected, and some achievements not being achievable. 80 | 81 | ## Attributions 82 | 83 | - Thank you to pjeby for [monkey-around](https://github.com/pjeby/monkey-around) that I used to hook into events in Obsidian. 84 | - Thank you to marcusolsson for [obsidian-svelte](https://github.com/marcusolsson/obsidian-svelte) that I used for creating many of the UI elements. 85 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | import esbuildSvelte from "esbuild-svelte"; 5 | import sveltePreprocess from "svelte-preprocess"; 6 | 7 | const banner = `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | `; 12 | 13 | const prod = process.argv[2] === "production"; 14 | 15 | esbuild 16 | .build({ 17 | plugins: [ 18 | esbuildSvelte({ 19 | compilerOptions: { css: true }, 20 | preprocess: sveltePreprocess(), 21 | }), 22 | ], 23 | banner: { 24 | js: banner, 25 | }, 26 | entryPoints: ["src/main.ts"], 27 | bundle: true, 28 | external: [ 29 | "obsidian", 30 | "electron", 31 | "@codemirror/autocomplete", 32 | "@codemirror/collab", 33 | "@codemirror/commands", 34 | "@codemirror/language", 35 | "@codemirror/lint", 36 | "@codemirror/search", 37 | "@codemirror/state", 38 | "@codemirror/view", 39 | "@lezer/common", 40 | "@lezer/highlight", 41 | "@lezer/lr", 42 | ...builtins, 43 | ], 44 | format: "cjs", 45 | watch: !prod, 46 | target: "es2018", 47 | logLevel: "info", 48 | sourcemap: prod ? false : "inline", 49 | treeShaking: true, 50 | outfile: "main.js", 51 | }) 52 | .catch(() => process.exit(1)); 53 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-achievements", 3 | "name": "Achievements", 4 | "version": "0.0.10", 5 | "minAppVersion": "0.15.0", 6 | "description": "Add achievements to Obsidian.", 7 | "author": "Zachatoo", 8 | "authorUrl": "https://zachyoung.dev", 9 | "fundingUrl": "https://github.com/sponsors/Zachatoo", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-achievements", 3 | "version": "0.0.10", 4 | "description": "Add achievements to Obsidian.", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "test": "vitest", 9 | "test:ci": "vitest run", 10 | "typecheck": "tsc -noEmit -skipLibCheck", 11 | "lint": "npx eslint . --ext .ts,.svelte", 12 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 13 | "version": "node version-bump.mjs && git add manifest.json versions.json", 14 | "prepare": "husky install" 15 | }, 16 | "keywords": [ 17 | "obsidian-plugin" 18 | ], 19 | "author": "Zachatoo", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@popperjs/core": "^2.11.6", 23 | "@tsconfig/svelte": "^3.0.0", 24 | "@types/node": "^16.11.6", 25 | "@typescript-eslint/eslint-plugin": "^5.40.1", 26 | "@typescript-eslint/parser": "^5.40.1", 27 | "builtin-modules": "3.3.0", 28 | "esbuild": "0.14.47", 29 | "esbuild-svelte": "^0.7.1", 30 | "eslint-plugin-svelte3": "^4.0.0", 31 | "husky": "^7.0.0", 32 | "obsidian": "latest", 33 | "obsidian-svelte": "^0.0.28", 34 | "svelte": "^3.52.0", 35 | "svelte-portal": "^2.2.0", 36 | "svelte-preprocess": "^4.10.7", 37 | "tslib": "2.4.0", 38 | "typescript": "4.7.4", 39 | "vitest": "^0.24.3" 40 | }, 41 | "dependencies": { 42 | "monkey-around": "^2.3.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/InternalCounts.ts: -------------------------------------------------------------------------------- 1 | export interface InternalCounts { 2 | noteCount: number; 3 | internalLinkCount: number; 4 | tagCount: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/markdownHelpers.test.ts: -------------------------------------------------------------------------------- 1 | import type { CachedMetadata } from "obsidian"; 2 | import { fileHasCallout, getFileHeadingLevelsCount } from "src/markdownHelpers"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | describe("markdownHelpers", () => { 6 | describe("fileHasCallout", () => { 7 | it("returns true if there is a callout", () => { 8 | const mockCache: CachedMetadata = { 9 | sections: [ 10 | { 11 | type: "callout", 12 | position: { 13 | end: { line: 2, col: 10, offset: 27 }, 14 | start: { line: 1, col: 0, offset: 1 }, 15 | }, 16 | }, 17 | ], 18 | }; 19 | 20 | const result = fileHasCallout(mockCache); 21 | 22 | expect(result).toBe(true); 23 | }); 24 | 25 | it("returns false for empty cache", () => { 26 | const mockCache: CachedMetadata = {}; 27 | 28 | const result = fileHasCallout(mockCache); 29 | 30 | expect(result).toBe(false); 31 | }); 32 | 33 | it("returns false if there are no callouts", () => { 34 | const mockCache: CachedMetadata = { 35 | sections: [ 36 | { 37 | position: { 38 | end: { line: 0, col: 9, offset: 9 }, 39 | start: { line: 0, col: 0, offset: 0 }, 40 | }, 41 | type: "heading", 42 | }, 43 | { 44 | position: { 45 | end: { line: 2, col: 7, offset: 18 }, 46 | start: { line: 2, col: 0, offset: 11 }, 47 | }, 48 | type: "paragraph", 49 | }, 50 | ], 51 | }; 52 | 53 | const result = fileHasCallout(mockCache); 54 | 55 | expect(result).toBe(false); 56 | }); 57 | }); 58 | 59 | describe("getFileHeadingLevelsCount", () => { 60 | it("returns 0 if no headings", () => { 61 | const mockCache: CachedMetadata = { 62 | sections: [ 63 | { 64 | type: "callout", 65 | position: { 66 | end: { line: 2, col: 10, offset: 27 }, 67 | start: { line: 1, col: 0, offset: 1 }, 68 | }, 69 | }, 70 | ], 71 | }; 72 | 73 | const result = getFileHeadingLevelsCount(mockCache); 74 | 75 | expect(result).toBe(0); 76 | }); 77 | 78 | it("returns 0 if empty cache", () => { 79 | const mockCache: CachedMetadata = {}; 80 | 81 | const result = getFileHeadingLevelsCount(mockCache); 82 | 83 | expect(result).toBe(0); 84 | }); 85 | 86 | it("returns 1 if single heading", () => { 87 | const mockCache: CachedMetadata = { 88 | headings: [ 89 | { 90 | heading: "Heading", 91 | level: 1, 92 | position: { 93 | end: { line: 0, col: 9, offset: 9 }, 94 | start: { line: 0, col: 0, offset: 0 }, 95 | }, 96 | }, 97 | ], 98 | }; 99 | 100 | const result = getFileHeadingLevelsCount(mockCache); 101 | 102 | expect(result).toBe(1); 103 | }); 104 | 105 | it("returns 1 if 2 headings same level", () => { 106 | const mockCache: CachedMetadata = { 107 | headings: [ 108 | { 109 | heading: "Heading", 110 | level: 1, 111 | position: { 112 | end: { line: 0, col: 9, offset: 9 }, 113 | start: { line: 0, col: 0, offset: 0 }, 114 | }, 115 | }, 116 | { 117 | heading: "Another heading", 118 | level: 1, 119 | position: { 120 | end: { line: 4, col: 17, offset: 37 }, 121 | start: { line: 4, col: 0, offset: 20 }, 122 | }, 123 | }, 124 | ], 125 | }; 126 | 127 | const result = getFileHeadingLevelsCount(mockCache); 128 | 129 | expect(result).toBe(1); 130 | }); 131 | 132 | it("returns 2 if 2 headings different levels", () => { 133 | const mockCache: CachedMetadata = { 134 | headings: [ 135 | { 136 | heading: "Heading", 137 | level: 1, 138 | position: { 139 | end: { line: 0, col: 9, offset: 9 }, 140 | start: { line: 0, col: 0, offset: 0 }, 141 | }, 142 | }, 143 | { 144 | heading: "Another heading", 145 | level: 2, 146 | position: { 147 | end: { line: 4, col: 18, offset: 38 }, 148 | start: { line: 4, col: 0, offset: 20 }, 149 | }, 150 | }, 151 | ], 152 | }; 153 | 154 | const result = getFileHeadingLevelsCount(mockCache); 155 | 156 | expect(result).toBe(2); 157 | }); 158 | 159 | it("returns 6 if 6 headings different levels", () => { 160 | const mockCache: CachedMetadata = { 161 | headings: [ 162 | { 163 | position: { 164 | start: { line: 0, col: 0, offset: 0 }, 165 | end: { line: 0, col: 13, offset: 13 }, 166 | }, 167 | heading: "First level", 168 | level: 1, 169 | }, 170 | { 171 | position: { 172 | start: { line: 2, col: 0, offset: 15 }, 173 | end: { line: 2, col: 15, offset: 30 }, 174 | }, 175 | heading: "Second level", 176 | level: 2, 177 | }, 178 | { 179 | position: { 180 | start: { line: 4, col: 0, offset: 32 }, 181 | end: { line: 4, col: 15, offset: 47 }, 182 | }, 183 | heading: "Third level", 184 | level: 3, 185 | }, 186 | { 187 | position: { 188 | start: { line: 6, col: 0, offset: 49 }, 189 | end: { line: 6, col: 17, offset: 66 }, 190 | }, 191 | heading: "Fourth level", 192 | level: 4, 193 | }, 194 | { 195 | position: { 196 | start: { line: 8, col: 0, offset: 68 }, 197 | end: { line: 8, col: 17, offset: 85 }, 198 | }, 199 | heading: "Fifth level", 200 | level: 5, 201 | }, 202 | { 203 | position: { 204 | start: { line: 10, col: 0, offset: 87 }, 205 | end: { line: 10, col: 18, offset: 105 }, 206 | }, 207 | heading: "Sixth level", 208 | level: 6, 209 | }, 210 | ], 211 | }; 212 | 213 | const result = getFileHeadingLevelsCount(mockCache); 214 | 215 | expect(result).toBe(6); 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /src/achievements-view/AchievementsView.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#each sortAchievements(SEEDED_ACHIEVEMENTS, $settingsStore) as achievement} 9 | 10 | 18 | 19 | {/each} 20 | -------------------------------------------------------------------------------- /src/achievements-view/AchievementsView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf } from "obsidian"; 2 | import AchievementsViewComponent from "./AchievementsView.svelte"; 3 | 4 | export const VIEW_TYPE_ACHIEVEMENTS = "achievements-view"; 5 | 6 | export class AchievementsView extends ItemView { 7 | component: AchievementsViewComponent; 8 | 9 | constructor(leaf: WorkspaceLeaf) { 10 | super(leaf); 11 | } 12 | 13 | getViewType() { 14 | return VIEW_TYPE_ACHIEVEMENTS; 15 | } 16 | 17 | getDisplayText() { 18 | return "Achievements"; 19 | } 20 | 21 | getIcon() { 22 | return "trophy"; 23 | } 24 | 25 | async onOpen() { 26 | this.component = new AchievementsViewComponent({ 27 | target: this.contentEl, 28 | }); 29 | } 30 | 31 | async onClose() { 32 | this.component.$destroy(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/array-utils/sort.ts: -------------------------------------------------------------------------------- 1 | import type { Achievement } from "src/seededAchievements"; 2 | import type { Settings } from "src/settings"; 3 | 4 | export function sortAchievements(arr: Achievement[], settings: Settings) { 5 | const result = [...arr]; 6 | return result.sort((a, b) => { 7 | const achievedCriteria = 8 | (settings.achievedAchievementIDs.includes(a.id) ? 1 : 0) - 9 | (settings.achievedAchievementIDs.includes(b.id) ? 1 : 0); 10 | if (achievedCriteria !== 0) { 11 | return achievedCriteria; 12 | } 13 | 14 | const aProgress = settings[a.type] / a.requiredOccurenceCount; 15 | const bProgress = settings[b.type] / b.requiredOccurenceCount; 16 | const progressCriteria = bProgress - aProgress; 17 | if (progressCriteria !== 0) { 18 | return progressCriteria; 19 | } 20 | 21 | const requiredOccurenceCountCriteria = 22 | a.requiredOccurenceCount - b.requiredOccurenceCount; 23 | if (requiredOccurenceCountCriteria !== 0) { 24 | return requiredOccurenceCountCriteria; 25 | } 26 | 27 | return a.name.localeCompare(b.name); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { around } from "monkey-around"; 2 | import type { Command } from "obsidian"; 3 | 4 | export function onCommandTrigger(id: string, cb: () => void) { 5 | const uninstallCommand = around(this.app.commands, { 6 | executeCommand(originalMethod) { 7 | return function (...args: Command[]) { 8 | if (args[0].id === id) { 9 | cb(); 10 | } 11 | const result = 12 | originalMethod && originalMethod.apply(this, args); 13 | return result; 14 | }; 15 | }, 16 | }); 17 | return uninstallCommand; 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Plugin, TAbstractFile, type CachedMetadata } from "obsidian"; 2 | import { get } from "svelte/store"; 3 | import { AchievementsSettingTab } from "./settings-tab/SettingsTab"; 4 | import { 5 | SEEDED_ACHIEVEMENTS, 6 | type AchievementType, 7 | } from "./seededAchievements"; 8 | import { onCommandTrigger } from "./commands"; 9 | import settingsStore, { type Settings } from "./settings"; 10 | import { fileHasCallout, getFileHeadingLevelsCount } from "./markdownHelpers"; 11 | import type { InternalCounts } from "./InternalCounts"; 12 | import { 13 | AchievementsView, 14 | VIEW_TYPE_ACHIEVEMENTS, 15 | } from "./achievements-view/AchievementsView"; 16 | 17 | export default class AchievementsPlugin extends Plugin { 18 | internalCounts: InternalCounts; 19 | 20 | async onload() { 21 | console.log("loading Achievements plugin"); 22 | 23 | settingsStore.init(await this.loadData()); 24 | 25 | this.register( 26 | settingsStore.subscribe(async (settings) => { 27 | await this.saveData(settings); 28 | }) 29 | ); 30 | 31 | this.setupInternalCounts(); 32 | 33 | this.registerView( 34 | VIEW_TYPE_ACHIEVEMENTS, 35 | (leaf) => new AchievementsView(leaf) 36 | ); 37 | 38 | this.registerEvent( 39 | this.app.metadataCache.on("changed", (file, data, cache) => { 40 | this.handleFileCreateUpdateDelete(file, cache); 41 | }) 42 | ); 43 | 44 | this.registerEvent( 45 | this.app.metadataCache.on("deleted", (file, _prevCache) => 46 | this.handleFileCreateUpdateDelete(file) 47 | ) 48 | ); 49 | 50 | this.register( 51 | onCommandTrigger("command-palette:open", async () => { 52 | const settings = this.getSettings(); 53 | settings.commandPaletteOpened += 1; 54 | this.getNewAchievementMaybe("commandPaletteOpened", settings); 55 | this.setSettings(settings); 56 | }) 57 | ); 58 | 59 | this.register( 60 | onCommandTrigger("switcher:open", async () => { 61 | const settings = this.getSettings(); 62 | settings.quickSwitcherOpened += 1; 63 | this.getNewAchievementMaybe("quickSwitcherOpened", settings); 64 | this.setSettings(settings); 65 | }) 66 | ); 67 | 68 | this.addCommand({ 69 | id: "show-achievements-view", 70 | name: "Show Achievements Panel", 71 | callback: () => { 72 | this.activateView(); 73 | }, 74 | }); 75 | 76 | this.addSettingTab(new AchievementsSettingTab(this.app, this)); 77 | } 78 | 79 | onunload() { 80 | console.log("unloading Achievements plugin"); 81 | 82 | this.app.workspace.detachLeavesOfType(VIEW_TYPE_ACHIEVEMENTS); 83 | } 84 | 85 | getSettings() { 86 | return get(settingsStore); 87 | } 88 | 89 | setSettings(settings: Settings) { 90 | settingsStore.set(settings); 91 | } 92 | 93 | setupInternalCounts() { 94 | this.internalCounts = { 95 | noteCount: this.getMarkdownFilesCount(), 96 | internalLinkCount: this.getInternalLinksCount(), 97 | tagCount: this.getTagsCount(), 98 | }; 99 | } 100 | 101 | async activateView() { 102 | this.app.workspace.detachLeavesOfType(VIEW_TYPE_ACHIEVEMENTS); 103 | 104 | await this.app.workspace.getRightLeaf(false).setViewState({ 105 | type: VIEW_TYPE_ACHIEVEMENTS, 106 | active: true, 107 | }); 108 | 109 | this.app.workspace.revealLeaf( 110 | this.app.workspace.getLeavesOfType(VIEW_TYPE_ACHIEVEMENTS)[0] 111 | ); 112 | } 113 | 114 | getMarkdownFilesCount() { 115 | return this.app.vault.getMarkdownFiles().length; 116 | } 117 | 118 | getInternalLinksCount() { 119 | let count = 0; 120 | this.app.metadataCache.iterateReferences(() => { 121 | count++; 122 | }); 123 | return count; 124 | } 125 | 126 | getTagsCount() { 127 | const tagsObj = this.app.metadataCache.getTags(); 128 | const baseTagsArr = Object.entries(tagsObj).filter( 129 | ([key]) => !key.includes("/") 130 | ); 131 | return baseTagsArr.reduce((prev, curr) => prev + curr[1], 0); 132 | } 133 | 134 | async handleFileCreateUpdateDelete( 135 | file: TAbstractFile, 136 | cache?: CachedMetadata 137 | ) { 138 | const currNoteCount = this.getMarkdownFilesCount(); 139 | const currInternalLinkCount = this.getInternalLinksCount(); 140 | const currTagsCount = this.getTagsCount(); 141 | const settings = this.getSettings(); 142 | 143 | if (currNoteCount > this.internalCounts.noteCount) { 144 | settings.notesCreated += 145 | currNoteCount - this.internalCounts.noteCount; 146 | this.internalCounts.noteCount = currNoteCount; 147 | this.getNewAchievementMaybe("notesCreated", settings); 148 | } 149 | if (currNoteCount < this.internalCounts.noteCount) { 150 | settings.notesDeleted += 151 | this.internalCounts.noteCount - currNoteCount; 152 | this.internalCounts.noteCount = currNoteCount; 153 | this.getNewAchievementMaybe("notesDeleted", settings); 154 | } 155 | if (currInternalLinkCount > this.internalCounts.internalLinkCount) { 156 | settings.internalLinksCreated += 157 | currInternalLinkCount - this.internalCounts.internalLinkCount; 158 | this.internalCounts.internalLinkCount = currInternalLinkCount; 159 | this.getNewAchievementMaybe("internalLinksCreated", settings); 160 | } 161 | 162 | if (currTagsCount > this.internalCounts.tagCount) { 163 | settings.tagsCreated += 164 | currTagsCount - this.internalCounts.tagCount; 165 | this.internalCounts.tagCount = currTagsCount; 166 | this.getNewAchievementMaybe("tagsCreated", settings); 167 | } 168 | 169 | if (cache) { 170 | if (settings.calloutsCreated === 0 && fileHasCallout(cache)) { 171 | settings.calloutsCreated = 1; 172 | this.getNewAchievementMaybe("calloutsCreated", settings); 173 | } 174 | 175 | const headingLevelsCount = getFileHeadingLevelsCount(cache); 176 | if (headingLevelsCount > settings.headingLevelsCreated) { 177 | settings.headingLevelsCreated = headingLevelsCount; 178 | this.getNewAchievementMaybe("headingLevelsCreated", settings); 179 | } 180 | } 181 | 182 | this.setSettings(settings); 183 | } 184 | 185 | getNewAchievementMaybe(type: AchievementType, settings: Settings) { 186 | const newAchievements = SEEDED_ACHIEVEMENTS.filter( 187 | (achievement) => 188 | achievement.type === type && 189 | settings[type] >= achievement.requiredOccurenceCount && 190 | !settings.achievedAchievementIDs.includes(achievement.id) 191 | ); 192 | if (newAchievements.length > 0) { 193 | newAchievements.forEach((achievement) => { 194 | settings.achievedAchievementIDs.push(achievement.id); 195 | new Notice(`${achievement.name}\n${achievement.popupMessage}`); 196 | }); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/markdownHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { CachedMetadata } from "obsidian"; 2 | 3 | export function fileHasCallout(cache: CachedMetadata) { 4 | if (!cache.sections) { 5 | return false; 6 | } 7 | return cache.sections.some((section) => section.type === "callout"); 8 | } 9 | 10 | export function getFileHeadingLevelsCount(cache: CachedMetadata) { 11 | if (!cache.headings) { 12 | return 0; 13 | } 14 | 15 | const levels = cache.headings.reduce((obj, { level }) => { 16 | obj[level] = true; 17 | return obj; 18 | }, {} as { [key: string]: boolean }); 19 | 20 | return Object.keys(levels).length; 21 | } 22 | -------------------------------------------------------------------------------- /src/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | 3 | declare module "obsidian" { 4 | interface App { 5 | commands: Commands; 6 | } 7 | 8 | interface Commands { 9 | executeCommand: (command: Command) => boolean; 10 | } 11 | 12 | interface MetadataCache { 13 | getTags: () => { [key: string]: number }; 14 | iterateReferences: ( 15 | callback: (sourcePath: string, reference: ReferenceCache) => any 16 | ) => any; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/reset-progress-modal/ResetProgressModal.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

Reset Progress

9 | 10 |

Resets all achievement progress. THIS CANNOT BE UNDONE.

11 |

Are you sure you want to continue?

12 | 13 |
14 | 15 | 16 |
-------------------------------------------------------------------------------- /src/reset-progress-modal/ResetProgressModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from "obsidian"; 2 | import settingsStore from "src/settings"; 3 | import ResetProgressModalComponent from "./ResetProgressModal.svelte"; 4 | 5 | export class ResetProgressModal extends Modal { 6 | component: ResetProgressModalComponent; 7 | 8 | constructor(app: App) { 9 | super(app); 10 | } 11 | 12 | onOpen() { 13 | this.component = new ResetProgressModalComponent({ 14 | target: this.contentEl, 15 | props: { 16 | close: () => this.close(), 17 | closeAndReset: () => this.closeAndReset(), 18 | }, 19 | }); 20 | } 21 | 22 | async closeAndReset() { 23 | settingsStore.reset(); 24 | this.close(); 25 | } 26 | 27 | onClose() { 28 | const { contentEl } = this; 29 | contentEl.empty(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/seededAchievements.ts: -------------------------------------------------------------------------------- 1 | export type AchievementType = 2 | | "notesCreated" 3 | | "notesDeleted" 4 | | "internalLinksCreated" 5 | | "commandPaletteOpened" 6 | | "quickSwitcherOpened" 7 | | "calloutsCreated" 8 | | "headingLevelsCreated" 9 | | "tagsCreated"; 10 | 11 | export interface Achievement { 12 | id: string; 13 | type: AchievementType; 14 | name: string; 15 | description: string; 16 | popupMessage: string; // message that shows when achievement is achieved 17 | requiredOccurenceCount: number; 18 | } 19 | 20 | export const SEEDED_ACHIEVEMENTS: Achievement[] = [ 21 | { 22 | id: "notes-created:1", 23 | type: "notesCreated", 24 | name: "Your first note", 25 | description: "Create a note.", 26 | popupMessage: 27 | "You've created your first note. You're off to a great start!", 28 | requiredOccurenceCount: 1, 29 | }, 30 | { 31 | id: "notes-created:10", 32 | type: "notesCreated", 33 | name: "Note taker", 34 | description: "Create ten notes.", 35 | popupMessage: "You've created ten notes. Great job!", 36 | requiredOccurenceCount: 10, 37 | }, 38 | { 39 | id: "notes-created:100", 40 | type: "notesCreated", 41 | name: "Wordsmith", 42 | description: "Create one hundred notes.", 43 | popupMessage: 44 | "You've created one hundred notes! You're serious, aren't you?", 45 | requiredOccurenceCount: 100, 46 | }, 47 | { 48 | id: "notes-created:1000", 49 | type: "notesCreated", 50 | name: "Storyteller", 51 | description: "Create one thousand notes.", 52 | popupMessage: "You've created one thousand notes! That's impressive!", 53 | requiredOccurenceCount: 1000, 54 | }, 55 | { 56 | id: "notes-deleted:10", 57 | type: "notesDeleted", 58 | name: "Taking out the trash", 59 | description: "Delete ten notes.", 60 | popupMessage: "You've deleted 10 notes. Way to keep your vault tidy!", 61 | requiredOccurenceCount: 10, 62 | }, 63 | { 64 | id: "internal-links-created:1", 65 | type: "internalLinksCreated", 66 | name: "Linking your thinking", 67 | description: 68 | "Create an internal link. You can type [[ to begin creating a internal link.", 69 | popupMessage: 70 | "You've created your first internal link. It's all coming together!", 71 | requiredOccurenceCount: 1, 72 | }, 73 | { 74 | id: "internal-links-created:10", 75 | type: "internalLinksCreated", 76 | name: "Making connections", 77 | description: "Create ten internal links.", 78 | popupMessage: 79 | "You've created 10 internal links. Your graph is looking great!", 80 | requiredOccurenceCount: 10, 81 | }, 82 | { 83 | id: "internal-links-created:100", 84 | type: "internalLinksCreated", 85 | name: "Conspiracy theorist", 86 | description: "Create one hundred internal links", 87 | popupMessage: 88 | "You've created 100 internal links. Your graph is starting to look like a conspiracy board...", 89 | requiredOccurenceCount: 100, 90 | }, 91 | { 92 | id: "internal-links-created:1000", 93 | type: "internalLinksCreated", 94 | name: "Air traffic controller", 95 | description: "Create one thousand internal links", 96 | popupMessage: 97 | "You've created 1000 internal links. If you haven't already, you should post your graph on the official Obsidian discord.", 98 | requiredOccurenceCount: 1000, 99 | }, 100 | { 101 | id: "command-palette:open", 102 | type: "commandPaletteOpened", 103 | name: "Commander", 104 | description: 105 | "Open the command palette. You can find the hotkey to open the command palette in Settings - Hotkeys.", 106 | popupMessage: 107 | "You've opened the command palette. Way to take charge of your note taking!", 108 | requiredOccurenceCount: 1, 109 | }, 110 | { 111 | id: "switcher:open", 112 | type: "quickSwitcherOpened", 113 | name: "Quickly now", 114 | description: 115 | "Open the quick switcher. You can find the hotkey to open the quick switcher in Settings - Hotkeys.", 116 | popupMessage: "You've opened the quick switcher. Wow that was fast!", 117 | requiredOccurenceCount: 1, 118 | }, 119 | { 120 | id: "callouts:1", 121 | type: "calloutsCreated", 122 | name: "Callouts", 123 | description: 124 | "Create a callout. You can find the hotkey to create a callout in Settings - Hotkeys.", 125 | popupMessage: 126 | "You've created a callout. Just felt like that needed to be called out.", 127 | requiredOccurenceCount: 1, 128 | }, 129 | { 130 | id: "heading-levels:1", 131 | type: "headingLevelsCreated", 132 | name: "Headings", 133 | description: 134 | "Create a heading. You can create a heading by adding a new line to a note and typing # Heading.", 135 | popupMessage: 136 | "You've created a heading. Your notes are looking more organized already!", 137 | requiredOccurenceCount: 1, 138 | }, 139 | { 140 | id: "heading-levels:3", 141 | type: "headingLevelsCreated", 142 | name: "Nested headings", 143 | description: 144 | "Create at least three levels of headings in a single note.", 145 | popupMessage: 146 | "You've created at least 3 levels of nested headings. Your notes look so organized!", 147 | requiredOccurenceCount: 3, 148 | }, 149 | { 150 | id: "tags-created:1", 151 | type: "tagsCreated", 152 | name: "Your first tag", 153 | description: "Create a tag. You can create a tag by typing #tag.", 154 | popupMessage: "You've created your first tag!", 155 | requiredOccurenceCount: 1, 156 | }, 157 | { 158 | id: "tags-created:5", 159 | type: "tagsCreated", 160 | name: "Tagging apprentice", 161 | description: "Create five unique tags.", 162 | popupMessage: "You've created five unique tags!", 163 | requiredOccurenceCount: 5, 164 | }, 165 | { 166 | id: "tags-created:10", 167 | type: "tagsCreated", 168 | name: "Tagging expert", 169 | description: "Create ten unique tags.", 170 | popupMessage: "You've created ten unique tags!", 171 | requiredOccurenceCount: 10, 172 | }, 173 | ]; 174 | -------------------------------------------------------------------------------- /src/settings-tab/SettingsTab.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |

Achievements List

10 | 11 | {#each SEEDED_ACHIEVEMENTS as achievement} 12 | 13 | 21 | 22 | {/each} 23 | 24 |

Danger Zone

25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/settings-tab/SettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab } from "obsidian"; 2 | import type AchievementsPlugin from "src/main"; 3 | import { ResetProgressModal } from "src/reset-progress-modal/ResetProgressModal"; 4 | import SettingsTabComponent from "./SettingsTab.svelte"; 5 | 6 | export class AchievementsSettingTab extends PluginSettingTab { 7 | component: SettingsTabComponent; 8 | 9 | constructor(app: App, plugin: AchievementsPlugin) { 10 | super(app, plugin); 11 | } 12 | 13 | display(): void { 14 | const { containerEl } = this; 15 | containerEl.empty(); 16 | 17 | this.component = new SettingsTabComponent({ 18 | target: containerEl, 19 | props: { 20 | openResetModal: () => this.openResetModal(), 21 | }, 22 | }); 23 | } 24 | 25 | openResetModal() { 26 | new ResetProgressModal(this.app).open(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export interface Settings { 4 | achievedAchievementIDs: string[]; 5 | notesCreated: number; 6 | notesDeleted: number; 7 | internalLinksCreated: number; 8 | commandPaletteOpened: number; 9 | quickSwitcherOpened: number; 10 | calloutsCreated: number; 11 | headingLevelsCreated: number; 12 | tagsCreated: number; 13 | } 14 | 15 | const DEFAULT_SETTINGS: Settings = { 16 | achievedAchievementIDs: [], 17 | notesCreated: 0, 18 | notesDeleted: 0, 19 | internalLinksCreated: 0, 20 | commandPaletteOpened: 0, 21 | quickSwitcherOpened: 0, 22 | calloutsCreated: 0, 23 | headingLevelsCreated: 0, 24 | tagsCreated: 0, 25 | }; 26 | 27 | function createStore() { 28 | const { subscribe, set, update } = writable(); 29 | 30 | function init(settings: Settings) { 31 | const newSettings = Object.assign({}, DEFAULT_SETTINGS, settings); 32 | set(newSettings); 33 | } 34 | 35 | function reset() { 36 | update((store) => { 37 | store = { ...DEFAULT_SETTINGS }; 38 | return store; 39 | }); 40 | } 41 | 42 | return { 43 | subscribe, 44 | set, 45 | init, 46 | reset, 47 | }; 48 | } 49 | 50 | const store = createStore(); 51 | 52 | export default store; 53 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .achievements-plugin__reset-modal__button-container { 2 | display: flex; 3 | justify-content: end; 4 | gap: 0.5rem; 5 | } 6 | 7 | .achievements-plugin__progress { 8 | position: relative; 9 | } 10 | 11 | .achievements-plugin__progress::before { 12 | content: attr(value) "/" attr(max); 13 | position: absolute; 14 | top: -20px; 15 | left: 0px; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["svelte", "node"], 5 | "baseUrl": ".", 6 | "inlineSources": true, 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "allowJs": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "strictNullChecks": true, 15 | "lib": ["DOM", "ES5", "ES6", "ES7"] 16 | }, 17 | "include": ["**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.15.0", 3 | "0.0.2": "0.15.0", 4 | "0.0.3": "0.15.0", 5 | "0.0.4": "0.15.0", 6 | "0.0.5": "0.15.0", 7 | "0.0.6": "0.15.0", 8 | "0.0.7": "0.15.0", 9 | "0.0.8": "0.15.0", 10 | "0.0.9": "0.15.0", 11 | "0.0.10": "0.15.0" 12 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | // ... 6 | }, 7 | }); 8 | --------------------------------------------------------------------------------