Developer Guide
This page covers how to set up a local development environment, contribute changes, and submit a pull request.
Prerequisites
Repository Structure
penny-wallet/
├── src/
│ ├── main.ts ← plugin entry point
│ ├── types.ts ← shared types and constants
│ ├── i18n.ts ← translations (zh-TW, en)
│ ├── utils.ts ← shared view helpers
│ ├── io/
│ │ └── WalletFile.ts ← all file I/O and business logic
│ ├── modal/
│ │ ├── TransactionModal.ts ← add/edit transaction form (desktop)
│ │ ├── MobileTransactionModal.ts ← add/edit transaction form (mobile)
│ │ └── ConfirmModal.ts ← shared confirmation dialog
│ ├── view/
│ │ ├── DashboardView.ts ← Finance Overview
│ │ ├── DetailView.ts ← Transactions list
│ │ └── AssetView.ts ← Assets view
│ └── settings/
│ └── SettingTab.ts ← plugin settings UI
├── scripts/
│ ├── generate-demo-data.mjs ← seed demo vault with realistic data
│ └── test-ui.mjs ← automated UI test runner (Obsidian CLI)
├── demo-vault/ ← local Obsidian vault for development
├── esbuild.config.mjs ← build configuration
├── manifest.json ← Obsidian plugin manifest
└── styles.css ← all plugin stylesSetup
Fork the repository, then clone your fork:
git clone https://github.com/<your-username>/penny-wallet.git
cd penny-wallet
npm installDevelopment Workflow
1. Start the watch build
npm run dev:watchThis runs esbuild in watch mode. On every file save:
- TypeScript is compiled and bundled to
main.js main.js,manifest.json, andstyles.cssare automatically copied todemo-vault/.obsidian/plugins/penny-wallet/
2. Open the demo vault in Obsidian
In Obsidian, open the demo-vault/ folder as a vault. The PennyWallet plugin is pre-configured there.
Enable the plugin if not already: Settings → Community Plugins → PennyWallet → Enable
Hot reload: After each build, use the Obsidian command Reload app without saving.
3. Reset and reseed
If the demo vault state gets messy, do a full reset:
npm run demo:resetThis removes all generated files from demo-vault/ (git clean -fdx), restores the tracked community-plugins.json, rebuilds the plugin, and regenerates 12 months of demo data in one step.
To regenerate only the transaction data without wiping vault settings:
npm run demo:dataThis runs scripts/generate-demo-data.mjs, which creates 12 months of realistic dummy transactions with multiple account types. The seed is fixed (20260403) so output is deterministic.
Build Commands
| Command | Description |
|---|---|
npm run dev:watch | Watch mode — rebuild on save, sync to demo vault |
npm run dev | Single development build (with inline sourcemap) |
npm run build | Production build (minified, no sourcemap) |
npm run demo:data | Seed demo vault with 12 months of dummy data |
npm run demo:reset | Full reset: wipe demo vault, rebuild plugin, reseed data |
npm run lint | ESLint + TypeScript type-check |
npm run lint:fix | ESLint autofix + type-check |
npm run test | Run unit + integration tests (Vitest) |
npm run test:ui | Run UI integration tests (requires Obsidian running) |
Type Checking
Run the TypeScript compiler in check-only mode (no emit):
npm run lintOr just the type-checker:
npx tsc --noEmitThe project targets ES2018 with lib: ["ES2017", "DOM"]. Avoid using APIs not in those libs (e.g. Array.flat() — use a loop instead).
Linting
ESLint is configured via eslint.config.mjs. Rules are inherited from typescript-eslint recommended defaults.
# Check only
npm run lint
# Autofix (safe fixes only)
npm run lint:fixTesting
Run unit and integration tests:
npm testRun UI integration tests (Obsidian must be open with demo-vault):
npm run test:uiTests are written with Vitest and live in tests/. The UI tests use the Obsidian CLI to drive a live instance. See the Testing page for full coverage details.
Manual Test Checklist
Before opening a PR, verify the following:
Transactions
- [ ] Add expense / income / transfer / repayment — all save correctly
- [ ] Edit a transaction in the same month
- [ ] Edit a transaction changing its date to a different month (cross-month move)
- [ ] Delete a transaction — confirm dialog appears, balance updates
Accounts
- [ ] Add cash / bank / credit card account
- [ ] Edit account name and initial balance — balances recalculate
- [ ] Archive an account with transactions — disappears from modal, stays in history
- [ ] Unarchive an account — reappears in transaction form
- [ ] Delete an account with no transactions
Credit Card
- [ ] Record expense on credit card — debt increases
- [ ] Record repayment from bank to credit card — both balances update
- [ ] Net asset reflects credit card debt as negative
Finance Overview
- [ ] Correct month navigation (prev/next, future disabled)
- [ ] Income / expense / balance metrics are correct
- [ ] Account balances match expected values
- [ ] Pie charts render with correct proportions and hover highlight
Transactions View
- [ ] Type filter pills work
- [ ] Category dropdown appears and filters correctly
- [ ] Subtotals match filtered transactions
Assets View
- [ ] 3 / 6 / 12-month range selector switches data
- [ ] Account balances and net assets are correct
- [ ] Net asset trend chart renders and tooltip shows on hover
- [ ] Asset allocation pie appears with two or more positive cash/bank accounts
Settings
- [ ] Folder name change persists
- [ ] Default account change applies to new transaction modal
- [ ] Decimal places switch: new transactions accept decimals / integers correctly
- [ ] Custom categories: add, duplicate check, remove
Code Conventions
File organisation
- Business logic and file I/O live exclusively in
src/io/WalletFile.ts - UI rendering is split by view — each view file is self-contained
- Shared utilities go in
src/utils.ts; shared modal components insrc/modal/
i18n
- All user-facing strings must use
t('key')fromsrc/i18n.ts - Add keys to both
zh-TWandenblocks - Category keys (e.g.
food) are translated viatranslateCategory(), nott()
Styling
- All CSS classes are prefixed with
.pw-to avoid Obsidian namespace collisions - Use Obsidian CSS variables (
--text-muted,--interactive-accent, etc.) for theme compatibility - Inline styles are only acceptable for dynamically computed values (e.g. chart colours, canvas positioning)
TypeScript
- Avoid
as any— if you need it, add a comment explaining why getConfig()returns a direct reference to internal state — treat it as read-only; useupdateConfig()to mutate- New event names must follow the
penny-wallet:<event>convention
Submitting a Pull Request
- Create a branch from
dev(notmain):bashgit checkout dev git checkout -b feat/your-feature-name - Make your changes and run through the Manual Test Checklist
- Run all checks:bash
npm run lint npm test npm run test:ui # requires Obsidian running with demo-vault - Push your branch and open a PR against the
devbranch - Describe what you changed and why — include screenshots if the UI is affected
Note: Releases are managed by the maintainer. PRs merged to
devare batched into releases; do not tag or publish releases yourself.