Skip to content

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 styles

Setup

Fork the repository, then clone your fork:

bash
git clone https://github.com/<your-username>/penny-wallet.git
cd penny-wallet
npm install

Development Workflow

1. Start the watch build

bash
npm run dev:watch

This runs esbuild in watch mode. On every file save:

  1. TypeScript is compiled and bundled to main.js
  2. main.js, manifest.json, and styles.css are automatically copied to demo-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:

bash
npm run demo:reset

This 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:

bash
npm run demo:data

This 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

CommandDescription
npm run dev:watchWatch mode — rebuild on save, sync to demo vault
npm run devSingle development build (with inline sourcemap)
npm run buildProduction build (minified, no sourcemap)
npm run demo:dataSeed demo vault with 12 months of dummy data
npm run demo:resetFull reset: wipe demo vault, rebuild plugin, reseed data
npm run lintESLint + TypeScript type-check
npm run lint:fixESLint autofix + type-check
npm run testRun unit + integration tests (Vitest)
npm run test:uiRun UI integration tests (requires Obsidian running)

Type Checking

Run the TypeScript compiler in check-only mode (no emit):

bash
npm run lint

Or just the type-checker:

bash
npx tsc --noEmit

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

bash
# Check only
npm run lint

# Autofix (safe fixes only)
npm run lint:fix

Testing

Run unit and integration tests:

bash
npm test

Run UI integration tests (Obsidian must be open with demo-vault):

bash
npm run test:ui

Tests 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 in src/modal/

i18n

  • All user-facing strings must use t('key') from src/i18n.ts
  • Add keys to both zh-TW and en blocks
  • Category keys (e.g. food) are translated via translateCategory(), not t()

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; use updateConfig() to mutate
  • New event names must follow the penny-wallet:<event> convention

Submitting a Pull Request

  1. Create a branch from dev (not main):
    bash
    git checkout dev
    git checkout -b feat/your-feature-name
  2. Make your changes and run through the Manual Test Checklist
  3. Run all checks:
    bash
    npm run lint
    npm test
    npm run test:ui   # requires Obsidian running with demo-vault
  4. Push your branch and open a PR against the dev branch
  5. Describe what you changed and why — include screenshots if the UI is affected

Note: Releases are managed by the maintainer. PRs merged to dev are batched into releases; do not tag or publish releases yourself.

Released under the MIT License.