Scaffold from standard plugin template

Bootstrap obsidian-rss-importer from plugin-templates/standard: placeholders
substituted, isDesktopOnly set true, Turndown + jsdom test deps added, esbuild
bumped to 0.28.1 to clear advisory GHSA-gv7w-rqvm-qjhr (matches shell-path-copy).
Build and lint green; main.ts is a wiring-only stub pending feature phases.
This commit is contained in:
Charles Kelsoe 2026-06-14 13:57:19 -04:00
commit 35ac67a806
27 changed files with 10878 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

61
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,61 @@
name: Bug report
description: Report something that is not working as expected.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug.
**Before you post:** anything you write or attach here is **public**. Do not include passwords, personal file contents, or other sensitive information. Redact file paths and screenshots as needed. You post this information at your own discretion. See the [privacy policy](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/PRIVACY.md).
Security vulnerabilities must **not** be filed here. See [SECURITY.md](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/SECURITY.md).
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Describe the bug and what you expected instead.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: List the exact steps that trigger the bug.
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: input
id: obsidian-version
attributes:
label: Obsidian version
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin version
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Windows
- macOS
- Linux
- iOS / iPadOS
- Android
validations:
required: true
- type: checkboxes
id: confirm
attributes:
label: Confirmation
options:
- label: I have removed any sensitive information from this report.
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability (do not file publicly)
url: https://github.com/ckelsoe/obsidian-rss-importer/blob/main/SECURITY.md
about: Report security issues privately. Do not open a public issue.
- name: Privacy policy
url: https://github.com/ckelsoe/obsidian-rss-importer/blob/main/PRIVACY.md
about: What data the plugin does and does not handle.

View file

@ -0,0 +1,31 @@
name: Feature request
description: Suggest an idea or improvement.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for the suggestion.
**Note:** anything you write here is **public**. Do not include sensitive information.
- type: textarea
id: problem
attributes:
label: What problem would this solve?
description: Describe the use case or workflow gap.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe what you would like the plugin to do.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Other approaches or workarounds you have tried.
validations:
required: false

138
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,138 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Weekly OSV re-scan so new advisories surface even when no code changes
- cron: "0 12 * * 1"
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Type check
run: npx tsc --noEmit --skipLibCheck
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Validate Obsidian manifest
run: |
echo "Validating manifest.json..."
# Check required fields exist
for field in id name version minAppVersion description author; do
if ! jq -e ".$field" manifest.json > /dev/null 2>&1; then
echo "::error::Missing required field: $field"
exit 1
fi
done
# Check version format (semver)
VERSION=$(jq -r '.version' manifest.json)
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$ ]]; then
echo "::error::Invalid version format: $VERSION"
exit 1
fi
# Check versions.json exists and contains current version
if [ -f versions.json ]; then
if ! jq -e ".[\"$VERSION\"]" versions.json > /dev/null 2>&1; then
echo "::warning::Version $VERSION not found in versions.json"
fi
fi
echo "Manifest validation passed"
- name: Check for deprecated Obsidian API usage
run: |
echo "Scanning for known deprecated Obsidian APIs..."
FOUND=0
# workspace.activeLeaf — deprecated, use getActiveViewOfType()
if grep -n "\.activeLeaf\b" main.ts; then
echo "::error::Deprecated API: 'activeLeaf'. Use getActiveViewOfType() or getLeaf() instead."
FOUND=1
fi
# MarkdownRenderer.renderMarkdown — deprecated, use MarkdownRenderer.render()
if grep -n "renderMarkdown(" main.ts; then
echo "::error::Deprecated API: 'renderMarkdown'. Use MarkdownRenderer.render() instead."
FOUND=1
fi
# getLeaf() with a boolean argument — deprecated, use getLeaf('tab') etc.
if grep -n "getLeaf(true)\|getLeaf(false)" main.ts; then
echo "::error::Deprecated API: 'getLeaf(bool)'. Use getLeaf('tab') or getLeaf('split') instead."
FOUND=1
fi
# Notice.noticeEl — deprecated, use Notice.messageEl
if grep -n "\.noticeEl\b" main.ts; then
echo "::error::Deprecated API: 'noticeEl'. Use 'messageEl' instead."
FOUND=1
fi
if [ "$FOUND" -eq 0 ]; then
echo "No deprecated Obsidian API usage found."
else
exit 1
fi
- name: Check bundle size
run: |
SIZE=$(stat -c%s main.js 2>/dev/null || stat -f%z main.js)
echo "Bundle size: $SIZE bytes ($(echo "scale=2; $SIZE/1024" | bc) KB)"
if [ "$SIZE" -gt 500000 ]; then
echo "::warning::Bundle size exceeds 500KB"
fi
osv-scan:
name: OSV Scanner
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6
- name: Run OSV-Scanner
uses: google/osv-scanner-action/osv-scanner-action@v2.0.3
with:
scan-args: |-
--lockfile=package-lock.json
--recursive
dependency-review:
name: Dependency Review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
comment-summary-in-pr: on-failure

93
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,93 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
permissions:
contents: write
id-token: write
attestations: write
env:
PLUGIN_NAME: rss-importer
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Test
run: npm test
- name: Build plugin
run: npm run build
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v4
with:
subject-path: |
main.js
manifest.json
styles.css
- name: VirusTotal scan of release artifacts
# Best-effort: VirusTotal returns HTTP 409 ("resource already exists")
# when a byte-identical artifact was scanned before, and the action
# treats that as fatal. continue-on-error keeps a 409 (or any VirusTotal
# hiccup) from blocking the release; the scan still runs and reports.
continue-on-error: true
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
main.js
manifest.json
styles.css
- name: Extract release notes from CHANGELOG
id: notes
run: |
tag="${GITHUB_REF#refs/tags/}"
awk -v ver="$tag" '
$0 ~ "^## \\[" ver "\\]" { in_section=1; next }
in_section && /^## \[/ { exit }
in_section { print }
' CHANGELOG.md > release-notes.md
echo "Extracted release notes:"
cat release-notes.md
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
# Mark pre-release tags (-beta/-rc/-alpha) so Obsidian's marketplace
# updater and stable BRAT installs do not pick them up as the latest.
prerelease=""
case "$tag" in
*-beta*|*-rc*|*-alpha*) prerelease="--prerelease" ;;
esac
if [ -s release-notes.md ]; then
gh release create "$tag" $prerelease \
--title="$tag" \
--notes-file release-notes.md \
main.js manifest.json styles.css
else
gh release create "$tag" $prerelease \
--title="$tag" \
--generate-notes \
main.js manifest.json styles.css
fi

49
.gitignore vendored Normal file
View file

@ -0,0 +1,49 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Build output
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# AI assistant files
.claude
CLAUDE.md
WARP.md
# Local Obsidian workspace
.obsidian/
# Screenshots and images (add specific ones to repo if needed)
Selection_*.png
*.screenshot.png
# Backup files
main_backup.ts
*.backup
*.bak
# Review files
GeminiReview.md
*Review.md
# Temporary files
*.zip
*.tmp
# Internal documentation
internal-docs/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

13
CHANGELOG.md Normal file
View file

@ -0,0 +1,13 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - YYYY-MM-DD
### Added
- Initial release.

69
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,69 @@
# Contributing to RSS Importer
Thanks for your interest in improving RSS Importer. This guide explains how to get a local build running, propose changes, and submit pull requests.
## Reporting issues
- Search [existing issues](https://github.com/ckelsoe/obsidian-rss-importer/issues) before opening a new one.
- For bugs, include Obsidian version, platform (Windows/macOS/Linux/iOS/Android), plugin version, and reproduction steps.
- For feature requests, describe the use case and how it fits the plugin's scope.
## Development setup
1. Fork and clone the repo.
2. Install dependencies:
```bash
npm install
```
3. Start the dev build with file watching:
```bash
npm run dev
```
4. Copy `main.js`, `manifest.json`, and `styles.css` into a test vault under `.obsidian/plugins/rss-importer/`, then enable the plugin in Obsidian.
## Quality gates
Before opening a pull request, run:
```bash
npm run lint # ESLint with eslint-plugin-obsidianmd recommended, zero warnings allowed
npm run build # TypeScript strict type-check + esbuild production bundle
npm test # Jest unit tests
```
All three must pass. Pull requests that break CI will not be reviewed until green.
## Coding conventions
- TypeScript strict mode is on. Never use `as any` casting. If types are missing, add declarations to `types.d.ts`.
- Follow [Obsidian's plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Prefer `containerEl.createDiv()` / `createSpan()` over generic `createEl('div'|'span')`.
- Do not import Node built-ins (`path`, `fs`, etc.) at the top of `main.ts`. Use a `Platform.isDesktop`-guarded `require()` instead.
- No inline `style` attributes. Move styles to `styles.css`.
- Settings tab headings must avoid the words "settings", "options", "general", and the plugin name.
- All UI strings (commands, menu titles, setting names, notifications) use sentence case. Brands recognized by `eslint-plugin-obsidianmd` (Markdown, macOS, iOS, Windows, Linux, etc.) keep their official casing.
- Keep the settings UI compatible with mobile (`isDesktopOnly: false` means features must degrade gracefully on iOS/Android).
## Submitting a pull request
1. Create a feature branch off `main`.
2. Make focused, atomic commits.
3. Update `CHANGELOG.md` under an `## [Unreleased]` heading describing user-visible changes.
4. Push your branch and open a PR against `main`.
5. Fill out the PR description: what changed, why, and how to test it.
## Releases
Releases are cut by the maintainer. The release workflow runs automatically on tag push and:
1. Builds and tests the plugin.
2. Generates SLSA build provenance attestation for `main.js`, `manifest.json`, and `styles.css`.
3. Submits artifacts to VirusTotal for malware analysis (requires `VT_API_KEY` repo secret).
4. Extracts the matching `CHANGELOG.md` section as release notes.
5. Publishes the GitHub release.
Contributors do not need to bump versions or create release artifacts.
## License
By contributing you agree that your contributions are licensed under the MIT License (see `LICENSE`).

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Charles Kelsoe (ckelsoe)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
PRIVACY.md Normal file
View file

@ -0,0 +1,48 @@
# Privacy Policy
_Last updated: 2026-06-14_
This policy explains what the **RSS Importer** Obsidian plugin ("the plugin") does and does not do with your data. It applies to the plugin as distributed through the Obsidian Community Plugins marketplace, GitHub releases, and BRAT.
## Summary
The plugin collects nothing, stores nothing outside your own vault, and sends nothing anywhere. It contains no network code.
## What the plugin does
_Describe the plugin's data flow in plain terms here. Be specific about what files the plugin reads, what files it modifies, when it modifies them, and whether the modifications are triggered by an explicit user action._
## Data collection
- **No personal data is collected.** The plugin does not collect names, email addresses, file contents, usage statistics, or any other information.
- **No telemetry or analytics.** There is no tracking, crash reporting, or phone-home behavior of any kind.
- **No automatic background activity.** The plugin acts only when you explicitly invoke a command or trigger a documented event.
## Data storage
- The plugin's settings are stored by Obsidian in your vault's local `data.json` file, on your own device. They never leave your device.
- Any other data the plugin reads or writes stays inside your vault, on your own device.
## Network use
The plugin contains **no network code**. It makes no HTTP requests, opens no sockets, and contacts no servers. Not the maintainer's, not Obsidian's, not any third party.
## Third parties
The plugin shares no data with any third party. It has no data to share and no means to transmit it.
## Disclaimer of liability
The plugin is provided free of charge, "AS IS", without warranty of any kind, as set out in the [MIT License](./LICENSE). To the maximum extent permitted by law, the maintainer is not liable for any loss, damage, or claim arising from use of the plugin.
## Information you choose to share
If you open a GitHub issue, discussion, or pull request, anything you paste there (file contents, screenshots, vault structure, system details) becomes **public**. The maintainer does not request this information and is not responsible for content you choose to post. Review and redact anything sensitive before submitting. To report a security vulnerability privately instead, see [SECURITY.md](./SECURITY.md).
## Changes to this policy
This policy may be updated as the plugin evolves. Material changes will be noted in [CHANGELOG.md](./CHANGELOG.md). The "last updated" date above reflects the current version.
## Contact
Questions about this policy: open an issue at [github.com/ckelsoe/obsidian-rss-importer/issues](https://github.com/ckelsoe/obsidian-rss-importer/issues). Do not use a public issue for security vulnerabilities; see [SECURITY.md](./SECURITY.md) for the private reporting channel.

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# RSS Importer
[![CI](https://img.shields.io/github/actions/workflow/status/ckelsoe/obsidian-rss-importer/ci.yml?branch=main&label=CI&logo=github)](https://github.com/ckelsoe/obsidian-rss-importer/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/actions/workflow/status/ckelsoe/obsidian-rss-importer/release.yml?label=Release&logo=github)](https://github.com/ckelsoe/obsidian-rss-importer/actions/workflows/release.yml) [![GitHub Downloads](https://img.shields.io/github/downloads/ckelsoe/obsidian-rss-importer/total?logo=github&label=Downloads)](https://github.com/ckelsoe/obsidian-rss-importer/releases) [![GitHub Stars](https://img.shields.io/github/stars/ckelsoe/obsidian-rss-importer?style=flat&logo=github&label=Stars)](https://github.com/ckelsoe/obsidian-rss-importer) [![Obsidian](https://img.shields.io/badge/Obsidian-v1.5.0%2B-7C3AED?logo=obsidian&logoColor=white)](https://obsidian.md) [![License](https://img.shields.io/github/license/ckelsoe/obsidian-rss-importer)](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/LICENSE) [![Latest Release](https://img.shields.io/github/v/release/ckelsoe/obsidian-rss-importer?label=Latest)](https://github.com/ckelsoe/obsidian-rss-importer/releases/latest)
Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.
## Installation
### From Obsidian Community Plugins (recommended)
1. Open Obsidian settings.
2. Navigate to **Community plugins**.
3. Click **Browse**.
4. Search for **RSS Importer**.
5. Click **Install**, then **Enable**.
### Manual installation
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/ckelsoe/obsidian-rss-importer/releases/latest).
2. Create a folder named `rss-importer` in your vault's `.obsidian/plugins/` directory.
3. Copy the downloaded files into this folder.
4. Reload Obsidian.
5. Enable **RSS Importer** in Settings → Community plugins.
### BRAT (optional, for pre-release testing)
BRAT lets power users install pre-release builds before they reach the marketplace.
1. Install the **BRAT** plugin from Community Plugins.
2. Open BRAT settings and click **Add Beta Plugin**.
3. Enter: `https://github.com/ckelsoe/obsidian-rss-importer`
4. Enable **RSS Importer** in Settings → Community plugins.
## Development
See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, quality gates, and conventions.
## License
MIT. See [LICENSE](./LICENSE).

35
SECURITY.md Normal file
View file

@ -0,0 +1,35 @@
# Security Policy
## Supported Versions
Only the latest published version of RSS Importer receives security updates.
| Version | Supported |
| ------- | ------------------ |
| latest | :white_check_mark: |
| older | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability in RSS Importer, please report it **privately** so it can be fixed before public disclosure:
1. **DO NOT** open a public GitHub issue for security vulnerabilities.
2. Open the repository's **Security** tab and click **Report a vulnerability**, or use this direct link: <https://github.com/ckelsoe/obsidian-rss-importer/security/advisories/new>
3. Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
Reports submitted through GitHub private vulnerability reporting are visible only to you and the maintainer until an advisory is published.
### What to expect:
- Acknowledgment within 48 hours
- Assessment and response within 7 days
- Security patch released as soon as possible
- Credit given to reporter (unless you prefer to remain anonymous)
## Security Considerations
_Describe what the plugin does and does not do that's relevant to security: file reads, file writes, network access (none expected), external commands, etc._

97
esbuild.config.mjs Normal file
View file

@ -0,0 +1,97 @@
import esbuild from "esbuild";
import process from "process";
import fs from "node:fs/promises";
import path from "node:path";
import { builtinModules } from "node:module";
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");
// Auto-copy build artifacts into the workspace test vault so reloading Obsidian
// picks up changes without a manual copy step. Skips silently if the test vault
// or the plugin's folder under it does not exist (e.g., on CI).
const TEST_VAULT_PLUGINS_DIR = path.resolve(
process.cwd(),
"..",
"..",
"obs-test-vault",
".obsidian",
"plugins",
);
const copyToTestVaultPlugin = {
name: "copy-to-test-vault",
setup(build) {
build.onEnd(async (result) => {
if (result.errors.length > 0) return;
try {
const manifestText = await fs.readFile(
path.join(process.cwd(), "manifest.json"),
"utf8",
);
const manifest = JSON.parse(manifestText);
const targetDir = path.join(TEST_VAULT_PLUGINS_DIR, manifest.id);
try {
await fs.access(targetDir);
} catch {
return;
}
for (const f of ["main.js", "manifest.json", "styles.css"]) {
try {
await fs.copyFile(path.join(process.cwd(), f), path.join(targetDir, f));
} catch (e) {
if (e.code !== "ENOENT") throw e;
}
}
console.log(`[copy-to-test-vault] Synced to ${targetDir}`);
} catch (e) {
console.warn(`[copy-to-test-vault] Skipped: ${e.message}`);
}
});
},
};
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.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",
...builtinModules,
...builtinModules.map(m => `node:${m}`)],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
plugins: [copyToTestVaultPlugin],
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

47
eslint.config.mts Normal file
View file

@ -0,0 +1,47 @@
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,
...globals.node,
},
parserOptions: {
projectService: {
allowDefaultProject: [
"eslint.config.mts",
"manifest.json",
],
},
tsconfigRootDir: import.meta.dirname,
extraFileExtensions: [".json"],
},
},
},
...obsidianmd.configs.recommended,
{
files: ["__tests__/**/*.ts"],
languageOptions: {
globals: {
...globals.jest,
},
},
},
globalIgnores([
"node_modules",
"dist",
"main.js",
"scripts",
"esbuild.config.mjs",
"version-bump.mjs",
"versions.json",
"package.json",
"package-lock.json",
"tsconfig.json",
"jest.config.cjs",
]),
);

13
jest.config.cjs Normal file
View file

@ -0,0 +1,13 @@
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
transform: {
'^.+\\.ts$': ['ts-jest', {
tsconfig: {
// Override ESNext module to CommonJS for Jest compatibility
module: 'CommonJS',
},
}],
},
};

10
main.ts Normal file
View file

@ -0,0 +1,10 @@
import { Plugin } from "obsidian";
/**
* RSS Importer plugin entry point.
*
* The plugin class is wiring only. Command registration, the settings tab, and
* feed-source wiring are added across the build phases; sources and any
* network-dependent state are constructed in `app.workspace.onLayoutReady`.
*/
export default class RssImporterPlugin extends Plugin {}

11
manifest.json Normal file
View file

@ -0,0 +1,11 @@
{
"id": "rss-importer",
"name": "RSS Importer",
"version": "0.1.0",
"minAppVersion": "1.13.0",
"description": "Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.",
"author": "Charles Kelsoe (ckelsoe)",
"authorUrl": "https://github.com/ckelsoe",
"fundingUrl": "https://buymeacoffee.com/ckelsoe",
"isDesktopOnly": true
}

9877
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "rss-importer",
"version": "0.1.0",
"description": "Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint": "eslint . --max-warnings 0 && node scripts/check-submission.mjs",
"test": "jest",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "Charles Kelsoe (ckelsoe)",
"license": "MIT",
"dependencies": {
"turndown": "^7.2.4",
"turndown-plugin-gfm": "^1.0.2"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.7",
"@types/turndown": "^5.0.6",
"esbuild": "^0.28.1",
"eslint": "^9.39.2",
"eslint-plugin-obsidianmd": "^0.3.0",
"globals": "^17.1.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.4.1",
"jiti": "^2.6.1",
"obsidian": "^1.13.0",
"ts-jest": "^29.4.6",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.1"
},
"overrides": {
"handlebars": "^4.7.9",
"picomatch": "^2.3.2",
"flatted": "^3.4.0",
"ajv": "^6.14.0",
"brace-expansion@1": "^1.1.13",
"brace-expansion@2": "^2.0.3"
}
}

View file

@ -0,0 +1,97 @@
// Pre-submission guard for Obsidian's automated marketplace review.
//
// Checks a small set of review rules that the local eslint plugin
// (eslint-plugin-obsidianmd) does NOT cover and that are not in the prose
// developer docs, but ARE enforced by Obsidian's online developer-dashboard
// preview scan. This is a PRE-FILTER, not the authoritative gate: the preview
// scan is the real gate and must be run on the release commit before publishing.
//
// Exits non-zero on any finding so it can chain into `npm run lint` and CI.
import { readdirSync, readFileSync } from "node:fs";
const findings = [];
// Forbid suppressing any obsidianmd/* lint rule. Obsidian's developer-dashboard
// scan rejects disabling its rules, and local eslint cannot report its own
// suppressions (an `// eslint-disable obsidianmd/...` reads as zero local errors
// but fails the dashboard), so this guard scans source for them and fails the
// build. Comply with the rule (rename, restructure) instead of disabling it.
const CODE_EXT = /\.(ts|mts|cts|tsx|js|mjs|cjs)$/;
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "scripts"]);
// Anchored to a comment opener (// or /*) so prose that merely mentions
// "eslint-disable" is not flagged; eslint only honors directives at a comment's start.
const DISABLE_OBSIDIANMD = /(?:\/\/|\/\*)\s*eslint-disable(?:-next-line|-line)?[^\n]*\bobsidianmd\//;
// Every eslint directive comment must carry a `-- description` (the dashboard
// enforces eslint-comments/require-description, which local eslint does not). A
// directive line is compliant only if it also contains a `--` separator.
const ESLINT_DIRECTIVE = /(?:\/\/|\/\*)\s*eslint-(?:disable|enable)(?:-next-line|-line)?\b/;
function* walkCode(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = `${dir}/${entry.name}`;
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) yield* walkCode(path);
} else if (CODE_EXT.test(entry.name) && entry.name !== "main.js") {
yield path;
}
}
}
for (const file of walkCode(".")) {
const lines = readFileSync(file, "utf8").split(/\r?\n/);
lines.forEach((line, index) => {
const where = `${file.replace(/^\.\//, "")}:${index + 1}`;
if (DISABLE_OBSIDIANMD.test(line)) {
findings.push(`${where}: do not eslint-disable an obsidianmd/* rule; comply with it (rename or restructure) instead.`);
}
if (ESLINT_DIRECTIVE.test(line) && !line.includes("--")) {
findings.push(`${where}: eslint directive comment needs a "-- description" explaining why it is necessary.`);
}
});
}
// The manifest description must not contain the word "Obsidian" (the online
// review rejects it as redundant with the plugin-directory context), must be at
// most 250 characters, and must end with sentence punctuation.
try {
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const description = typeof manifest.description === "string" ? manifest.description : "";
if (/\bobsidian\b/i.test(description)) {
findings.push('manifest.json: the description must not contain the word "Obsidian".');
}
if (description.length > 250) {
findings.push(`manifest.json: the description is ${description.length} characters; the maximum is 250.`);
}
if (description && !/[.!?]$/.test(description.trim())) {
findings.push("manifest.json: the description must end with '.', '!' or '?'.");
}
} catch (error) {
findings.push(`manifest.json: could not read or parse (${error.message}).`);
}
// styles.css must not use !important (raise selector specificity instead). Block
// comments are blanked first so a comment that mentions the token is not flagged,
// while line numbers are preserved.
try {
const css = readFileSync("styles.css", "utf8");
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "));
withoutComments.split(/\r?\n/).forEach((line, index) => {
if (/!important/i.test(line)) {
findings.push(`styles.css:${index + 1}: avoid !important; raise selector specificity instead.`);
}
});
} catch {
// styles.css is optional; skip if absent.
}
if (findings.length > 0) {
console.error("Submission pre-check failed:");
for (const finding of findings) console.error(` - ${finding}`);
console.error("");
console.error("This is a pre-filter only. Run the Obsidian developer-dashboard preview scan");
console.error("on the release commit for the authoritative result.");
process.exit(1);
}
console.log("Submission pre-check passed (pre-filter only; the developer-dashboard preview scan is the authoritative gate).");

1
styles.css Normal file
View file

@ -0,0 +1 @@
/* RSS Importer styles. Selectors use Obsidian theme variables; no inline styles, no !important. */

33
tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"strictBindCallApply": true,
"allowSyntheticDefaultImports": true,
"useUnknownInCatchVariables": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2017",
"ES2018"
]
},
"include": [
"**/*.ts",
"**/*.d.ts"
]
}

11
types.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
// Local type augmentations for the Obsidian API.
// Add type declarations here when the Obsidian types are missing or need extension.
// NEVER use `as any` to work around missing types. Add a proper declaration here instead.
import 'obsidian';
declare module 'obsidian' {
interface PluginManifest {
version: string;
}
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "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 { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "1.13.0"
}