Compare commits

...

3 commits
1.5.0 ... main

Author SHA1 Message Date
Flying Nobita
b7a043454f
fix: release GitHub Stars 1.5.2 (#9)
Fix startup active-view warnings and keep automatic embedded-star updates scoped to their source notes.
2026-07-21 13:55:01 +08:00
Flying Nobita
bed0a1abd3
docs: add pull request template
Add a concise default PR template covering summary, changes, verification, and issue-closing references.
2026-03-27 13:04:56 +08:00
Flying Nobita
b4b9d71569
chore: release 1.5.1 (#6)
* chore: align package tooling with sample plugin

* chore: release 1.5.1
2026-03-27 01:25:37 +08:00
17 changed files with 2412 additions and 4664 deletions

View file

@ -1,3 +0,0 @@
node_modules/
main.js

View file

@ -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
View 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 #

View file

@ -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

View file

@ -1,7 +1,7 @@
# GitHub Stars Plugin
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.5.0-green?style=for-the-badge)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.5.2-green?style=for-the-badge)](CHANGELOG.md)
[![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2FHEAD%2Fcommunity-plugin-stats.json&query=%24%5B%22github-stars%22%5D.downloads&logo=obsidian&label=downloads&style=for-the-badge&color=7C3AED)](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

View file

@ -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.

View file

@ -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
View 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',
]),
);

View file

@ -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 };

View file

@ -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
View file

@ -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);

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
allowBuilds:
esbuild: true

View file

@ -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"
}