mirror of
https://github.com/flyingnobita/obsidian-github-stars.git
synced 2026-07-22 12:20:26 +00:00
Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a043454f | ||
|
|
bed0a1abd3 | ||
|
|
b4b9d71569 |
17 changed files with 2412 additions and 4664 deletions
|
|
@ -1,3 +0,0 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
23
.eslintrc
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
13
.github/pull_request_template.md
vendored
Normal file
13
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
## Summary
|
||||
Briefly describe the overall objective.
|
||||
|
||||
## Changes
|
||||
- Major change 1
|
||||
- Major change 2
|
||||
|
||||
## Verification
|
||||
- [ ] `pnpm test`
|
||||
- [ ] `pnpm build`
|
||||
|
||||
## Related Issue
|
||||
Closes #
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,5 +1,23 @@
|
|||
# Changelog
|
||||
|
||||
- Jul-21, 2026 - 01:45 PM +08 - [Released 1.5.2 with startup warning and source-note targeting fixes]
|
||||
|
||||
## [1.5.2] - 2026-07-21
|
||||
|
||||
### Fixed
|
||||
- Prevented the `No active markdown view found` notice during workspace startup
|
||||
- Scoped Reading View and Live Preview embedded-star updates to the note that triggered the refresh instead of whichever note is active when the debounced update runs
|
||||
|
||||
- Mar-27, 2026 - 01:23 AM +08 - [Released 1.5.1 with package tooling alignment and pnpm standardization]
|
||||
|
||||
## [1.5.1] - 2026-03-27
|
||||
|
||||
### Changed
|
||||
- Aligned the package manifest and lint toolchain with the current official Obsidian sample plugin
|
||||
- Standardized the repository on `pnpm`, removed `package-lock.json`, and documented `pnpm` commands in the README
|
||||
- Pinned CodeMirror packages to Obsidian's declared peer versions for cleaner installs
|
||||
- Added repo-local `pnpm` build approval for `esbuild`
|
||||
|
||||
- Mar-27, 2026 - 12:53 AM +08 - [Released 1.5.0 with vault-wide star maintenance commands and CI enforcement]
|
||||
|
||||
## [1.5.0] - 2026-03-27
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# GitHub Stars Plugin
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://obsidian.md/plugins?id=github-stars)
|
||||
|
||||
An Obsidian plugin that automatically displays GitHub star counts next to repository links in your notes — in both Reading View and Live Preview. Star counts can also be embedded directly into your markdown, making them visible outside Obsidian.
|
||||
|
|
@ -66,8 +66,8 @@ The plugin adds the following commands:
|
|||
|
||||
## 🧪 Development
|
||||
|
||||
- `npm test`: Runs the unit tests with Vitest
|
||||
- `npm run build`: Type-checks and builds the plugin bundle
|
||||
- `pnpm test`: Runs the unit tests with Vitest
|
||||
- `pnpm build`: Type-checks and builds the plugin bundle
|
||||
|
||||
## ❤️ Support This Project
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
- `Refresh for current note` only applies to the active note.
|
||||
- `Refresh for current note` fetches fresh star counts from GitHub for repositories in the active note even if their cache entries are still valid.
|
||||
- When `Update embedded stars on refresh` is enabled, refresh updates only links that already have embedded `⭐ ...` text in the active note and then rerenders the note.
|
||||
- When `Update embedded stars on refresh` is enabled, Reading View and Live Preview refresh paths also update existing embedded `⭐ ...` text for repositories whose star counts were refreshed.
|
||||
- When `Update embedded stars on refresh` is enabled, Reading View and Live Preview refresh paths update existing embedded `⭐ ...` text in the source note being rendered, even if another view becomes active before the update finishes.
|
||||
- Background rendering does not require an active Markdown view and does not show an active-view warning during workspace startup.
|
||||
- If the same repository appears multiple times in the active note, refresh fetches it once and applies the result to all matching occurrences.
|
||||
- When `Update embedded stars on refresh` is disabled, refresh rerenders the note without changing embedded `⭐ ...` text.
|
||||
- If a refresh fetch fails for a repository, existing embedded star text for that repository is left unchanged.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
|
|
@ -31,7 +31,7 @@ const context = await esbuild.context({
|
|||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
...builtinModules],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
|
|
|
|||
45
eslint.config.mts
Normal file
45
eslint.config.mts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import tseslint from 'typescript-eslint';
|
||||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
import globals from 'globals';
|
||||
import { globalIgnores } from 'eslint/config';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'eslint.config.mts',
|
||||
'manifest.json',
|
||||
],
|
||||
},
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
extraFileExtensions: ['.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ['main.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'obsidianmd/commands/no-plugin-id-in-command-id': 'off',
|
||||
'obsidianmd/ui/sentence-case': 'off',
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
'node_modules',
|
||||
'dist',
|
||||
'esbuild.config.mjs',
|
||||
'eslint.config.mjs',
|
||||
'version-bump.mjs',
|
||||
'versions.json',
|
||||
'main.js',
|
||||
]),
|
||||
);
|
||||
|
|
@ -4,11 +4,37 @@ import {
|
|||
extractUniqueReposFromContents,
|
||||
findEmbeddedGitHubLinkMatches,
|
||||
findGitHubLinkMatches,
|
||||
queueRepoUpdatesForSource,
|
||||
removeEmbeddedGitHubStars,
|
||||
rewriteGitHubLinksWithStars,
|
||||
shouldUseCachedEntry,
|
||||
} from './githubStarsCore';
|
||||
|
||||
describe('queueRepoUpdatesForSource', () => {
|
||||
it('keeps pending repository updates scoped to their source files', () => {
|
||||
const pendingUpdates = new Map<string, Set<string>>();
|
||||
|
||||
queueRepoUpdatesForSource(pendingUpdates, 'Notes/First.md', [
|
||||
{ owner: 'foo', repo: 'shared' },
|
||||
{ owner: 'foo', repo: 'first-only' },
|
||||
]);
|
||||
queueRepoUpdatesForSource(pendingUpdates, 'Notes/Second.md', [
|
||||
{ owner: 'foo', repo: 'shared' },
|
||||
]);
|
||||
queueRepoUpdatesForSource(pendingUpdates, 'Notes/First.md', [
|
||||
{ owner: 'foo', repo: 'shared' },
|
||||
]);
|
||||
|
||||
expect(Array.from(pendingUpdates.get('Notes/First.md') ?? [])).toEqual([
|
||||
'foo/shared',
|
||||
'foo/first-only',
|
||||
]);
|
||||
expect(Array.from(pendingUpdates.get('Notes/Second.md') ?? [])).toEqual([
|
||||
'foo/shared',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldUseCachedEntry', () => {
|
||||
it('uses a valid cache entry when refresh is not forced', () => {
|
||||
const entry = { stars: 42, timestamp: 1_000 };
|
||||
|
|
|
|||
|
|
@ -8,6 +8,22 @@ export interface RepoRef {
|
|||
repo: string;
|
||||
}
|
||||
|
||||
export function queueRepoUpdatesForSource(
|
||||
pendingUpdates: Map<string, Set<string>>,
|
||||
sourcePath: string,
|
||||
repos: RepoRef[]
|
||||
): void {
|
||||
let pendingRepoKeys = pendingUpdates.get(sourcePath);
|
||||
if (!pendingRepoKeys) {
|
||||
pendingRepoKeys = new Set<string>();
|
||||
pendingUpdates.set(sourcePath, pendingRepoKeys);
|
||||
}
|
||||
|
||||
for (const { owner, repo } of repos) {
|
||||
pendingRepoKeys.add(`${owner}/${repo}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitHubLinkMatch extends RepoRef {
|
||||
index: number;
|
||||
length: number;
|
||||
|
|
|
|||
79
main.ts
79
main.ts
|
|
@ -1,7 +1,7 @@
|
|||
import { App, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, MarkdownPostProcessorContext, requestUrl } from 'obsidian';
|
||||
import { App, editorInfoField, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, MarkdownPostProcessorContext, requestUrl, TFile } from 'obsidian';
|
||||
import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { extractReposFromContent, extractUniqueReposFromContents, findEmbeddedGitHubLinkMatches, findGitHubLinkMatches, GitHubLinkMatch, removeEmbeddedGitHubStars, RepoRef, rewriteGitHubLinksWithStars, shouldUseCachedEntry } from './githubStarsCore';
|
||||
import { extractReposFromContent, extractUniqueReposFromContents, findEmbeddedGitHubLinkMatches, findGitHubLinkMatches, GitHubLinkMatch, queueRepoUpdatesForSource, removeEmbeddedGitHubStars, RepoRef, rewriteGitHubLinksWithStars, shouldUseCachedEntry } from './githubStarsCore';
|
||||
|
||||
|
||||
interface GitHubStarsSettings {
|
||||
|
|
@ -39,12 +39,14 @@ interface StarCountFetchResult {
|
|||
*/
|
||||
function buildGitHubStarsViewPlugin(plugin: GitHubStarsPlugin) {
|
||||
class StarsWidget extends WidgetType {
|
||||
constructor(private owner: string, private repo: string) {
|
||||
constructor(private owner: string, private repo: string, private sourcePath: string | null) {
|
||||
super();
|
||||
}
|
||||
|
||||
eq(other: StarsWidget): boolean {
|
||||
return this.owner === other.owner && this.repo === other.repo;
|
||||
return this.owner === other.owner
|
||||
&& this.repo === other.repo
|
||||
&& this.sourcePath === other.sourcePath;
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
|
|
@ -70,7 +72,7 @@ function buildGitHubStarsViewPlugin(plugin: GitHubStarsPlugin) {
|
|||
span.removeClass('github-stars-loading');
|
||||
if (stars !== null) {
|
||||
span.setText(plugin.formatStarCount(stars));
|
||||
plugin.scheduleEmbeddedStarUpdate([{ owner: this.owner, repo: this.repo }]);
|
||||
plugin.scheduleEmbeddedStarUpdate([{ owner: this.owner, repo: this.repo }], this.sourcePath);
|
||||
} else {
|
||||
span.setText('⭐ ?');
|
||||
span.addClass('github-stars-error');
|
||||
|
|
@ -102,6 +104,7 @@ function buildGitHubStarsViewPlugin(plugin: GitHubStarsPlugin) {
|
|||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const sourcePath = view.state.field(editorInfoField, false)?.file?.path ?? null;
|
||||
const githubUrlRegex = /https?:\/\/(www\.)?github\.com\/([^/\s)#?]+)\/([^/\s)#?]+)(\/[^\s)]*)?\/?(#[^\s)]*)?/g;
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
|
|
@ -137,7 +140,7 @@ function buildGitHubStarsViewPlugin(plugin: GitHubStarsPlugin) {
|
|||
insertPos,
|
||||
insertPos,
|
||||
Decoration.widget({
|
||||
widget: new StarsWidget(owner, repo),
|
||||
widget: new StarsWidget(owner, repo, sourcePath),
|
||||
side: 1,
|
||||
})
|
||||
);
|
||||
|
|
@ -159,7 +162,7 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
activeRefreshRunId = 0;
|
||||
lastMissingTokenWarningRefreshId = 0;
|
||||
lastInvalidTokenWarningRefreshId = 0;
|
||||
pendingEmbeddedUpdateRepos = new Set<string>();
|
||||
pendingEmbeddedUpdates = new Map<string, Set<string>>();
|
||||
pendingEmbeddedUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
async onload() {
|
||||
|
|
@ -273,6 +276,12 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
onunload() {
|
||||
if (this.pendingEmbeddedUpdateTimer !== null) {
|
||||
clearTimeout(this.pendingEmbeddedUpdateTimer);
|
||||
this.pendingEmbeddedUpdateTimer = null;
|
||||
}
|
||||
this.pendingEmbeddedUpdates.clear();
|
||||
|
||||
// Save settings when plugin is unloaded
|
||||
this.saveSettings();
|
||||
}
|
||||
|
|
@ -349,14 +358,12 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
return this.settings.showTokenWarnings && this.activeRefreshRunId > 0;
|
||||
}
|
||||
|
||||
scheduleEmbeddedStarUpdate(repos: RepoRef[]): void {
|
||||
if (!this.settings.updateEmbeddedStarsOnRefresh || repos.length === 0) {
|
||||
scheduleEmbeddedStarUpdate(repos: RepoRef[], sourcePath: string | null): void {
|
||||
if (!this.settings.updateEmbeddedStarsOnRefresh || !sourcePath || repos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { owner, repo } of repos) {
|
||||
this.pendingEmbeddedUpdateRepos.add(this.getRepoCacheKey(owner, repo));
|
||||
}
|
||||
queueRepoUpdatesForSource(this.pendingEmbeddedUpdates, sourcePath, repos);
|
||||
|
||||
if (this.pendingEmbeddedUpdateTimer !== null) {
|
||||
return;
|
||||
|
|
@ -368,27 +375,33 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private async flushScheduledEmbeddedStarUpdates(): Promise<void> {
|
||||
const pendingKeys = new Set(this.pendingEmbeddedUpdateRepos);
|
||||
this.pendingEmbeddedUpdateRepos.clear();
|
||||
const pendingUpdates = new Map<string, Set<string>>();
|
||||
for (const [sourcePath, repoKeys] of this.pendingEmbeddedUpdates) {
|
||||
pendingUpdates.set(sourcePath, new Set(repoKeys));
|
||||
}
|
||||
this.pendingEmbeddedUpdates.clear();
|
||||
|
||||
if (this.pendingEmbeddedUpdateTimer !== null) {
|
||||
clearTimeout(this.pendingEmbeddedUpdateTimer);
|
||||
this.pendingEmbeddedUpdateTimer = null;
|
||||
}
|
||||
|
||||
if (pendingKeys.size === 0) {
|
||||
return;
|
||||
}
|
||||
for (const [sourcePath, pendingKeys] of pendingUpdates) {
|
||||
const file = this.app.vault.getFileByPath(sourcePath);
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.rewriteActiveNoteStars({
|
||||
findMatches: findEmbeddedGitHubLinkMatches,
|
||||
noLinksNotice: null,
|
||||
progressNotice: null,
|
||||
successNotice: null,
|
||||
fetchLatest: false,
|
||||
failureNotice: null,
|
||||
shouldRewriteMatch: (match) => pendingKeys.has(this.getRepoCacheKey(match.owner, match.repo)),
|
||||
});
|
||||
await this.rewriteNoteStars(file, {
|
||||
findMatches: findEmbeddedGitHubLinkMatches,
|
||||
noLinksNotice: null,
|
||||
progressNotice: null,
|
||||
successNotice: null,
|
||||
fetchLatest: false,
|
||||
failureNotice: null,
|
||||
shouldRewriteMatch: (match) => pendingKeys.has(this.getRepoCacheKey(match.owner, match.repo)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -433,7 +446,7 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
if (stars !== null) {
|
||||
const formattedStars = this.formatStarCount(stars);
|
||||
starSpan.setText(formattedStars);
|
||||
this.scheduleEmbeddedStarUpdate([repoInfo]);
|
||||
this.scheduleEmbeddedStarUpdate([repoInfo], ctx.sourcePath);
|
||||
} else {
|
||||
starSpan.setText('⭐ ?');
|
||||
starSpan.addClass('github-stars-error');
|
||||
|
|
@ -838,6 +851,18 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
return 0;
|
||||
}
|
||||
|
||||
return this.rewriteNoteStars(file, options);
|
||||
}
|
||||
|
||||
private async rewriteNoteStars(file: TFile, options: {
|
||||
findMatches: (content: string) => GitHubLinkMatch[];
|
||||
noLinksNotice: string | null;
|
||||
progressNotice: ((count: number) => string) | null;
|
||||
successNotice: ((count: number) => string) | null;
|
||||
fetchLatest: boolean;
|
||||
failureNotice: string | null;
|
||||
shouldRewriteMatch: (match: GitHubLinkMatch) => boolean;
|
||||
}): Promise<number> {
|
||||
const content = await this.app.vault.read(file);
|
||||
const matches = options.findMatches(content);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "github-stars",
|
||||
"name": "GitHub Stars",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"minAppVersion": "1.8.9",
|
||||
"description": "Displays the number of stars for GitHub repositories mentioned in notes.",
|
||||
"author": "Flying Nobita",
|
||||
"authorUrl": "https://github.com/flyingnobita",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4090
package-lock.json
generated
4090
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"name": "obsidian-github-stars",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"description": "Obsidian plugin that displays the number of stars for GitHub repositories mentioned in notes",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"test": "vitest run",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
|
|
@ -18,16 +20,21 @@
|
|||
"author": "Flying Nobita",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.37.1",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6",
|
||||
"@types/node": "^20.19.37",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"esbuild": "0.25.5",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-plugin-obsidianmd": "0.1.9",
|
||||
"globals": "14.0.0",
|
||||
"jiti": "2.6.1",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"obsidian": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2711
pnpm-lock.yaml
2711
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
allowBuilds:
|
||||
esbuild: true
|
||||
|
|
@ -6,5 +6,7 @@
|
|||
"1.2.1": "1.8.9",
|
||||
"1.3.0": "1.8.9",
|
||||
"1.4.0": "1.8.9",
|
||||
"1.5.0": "1.8.9"
|
||||
}
|
||||
"1.5.0": "1.8.9",
|
||||
"1.5.1": "1.8.9",
|
||||
"1.5.2": "1.8.9"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue