👨‍💻 Developer Guide

Welcome to the Make It Rain Developer Guide! This comprehensive documentation will help you set up a development environment, understand the codebase, and contribute to the project.


🎯 What’s in This Guide?

This guide covers everything you need to contribute:


⚡ Quick Start for Developers

Prerequisites

Before you start, ensure you have:

Get Set Up in 5 Minutes

# 1. Clone the repository
git clone https://github.com/frostmute/make-it-rain.git
cd make-it-rain

# 2. Install dependencies
npm install

# 3. Run tests to verify setup
npm test

# 4. Start development build
npm run dev

# 5. You're ready to code!

That’s it! Your environment is ready.


🏗️ Project Architecture

High-Level Overview

Make It Rain is an Obsidian plugin built with TypeScript that integrates with the Raindrop.io API to import bookmarks into your vault.

┌─────────────────────────────────────────────────────────┐
│                    Obsidian Vault                       │
│  (Note files, folder structure, metadata)               │
└────────────────────▲────────────────────────────────────┘
                     │ (Write files, create folders)
                     │
┌────────────────────┴────────────────────────────────────┐
│          Make It Rain Plugin (TypeScript)               │
│                                                         │
│  ┌─────────────────────────────────────────────────┐  │
│  │ User Interface (Settings, Modals)               │  │
│  │  - Settings Tab                                 │  │
│  │  - Fetch Modal                                  │  │
│  │  - Quick Import Modal                           │  │
│  └─────────────────────────────────────────────────┘  │
│                     ▲ (User interactions)               │
│                     │                                   │
│  ┌─────────────────┴─────────────────────────────────┐ │
│  │ Core Logic (Processors, Templates, Filters)      │ │
│  │  - Raindrop processing                           │ │
│  │  - Template rendering                            │ │
│  │  - Filter logic                                  │ │
│  │  - File/folder operations                        │ │
│  └────────────────┬─────────────────────────────────┘ │
│                   │                                    │
│  ┌────────────────▼─────────────────────────────────┐ │
│  │ API Layer (Rate limiting, Error handling)        │ │
│  │  - Raindrop.io API client                        │ │
│  │  - Rate limiting & retry logic                   │ │
│  │  - Error handling                                │ │
│  └──────────────────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────┘
                     │ (HTTP requests)
                     ▼
        ┌─────────────────────────────────┐
        │    Raindrop.io API              │
        │    (REST endpoints)             │
        └─────────────────────────────────┘

Core Modules

Module Purpose Key Files
UI Layer User interface components main.ts, settings.ts, modals.ts
API Client Raindrop.io integration utils/apiUtils.ts
Processing Core business logic main.ts, utils/
Templates Template parsing and rendering template-validator.ts, utils/templateUtils.ts
File I/O Vault file operations utils/fileUtils.ts
Types TypeScript definitions types.ts

Data Flow

1. User opens plugin or runs command
   ↓
2. Modal/Settings UI captures input
   ↓
3. Plugin processes user selections
   ↓
4. API layer fetches from Raindrop.io
   ↓
5. Processing layer:
   - Applies filters
   - Renders templates
   - Generates notes
   ↓
6. File I/O writes to vault
   ↓
7. User sees imported notes in Obsidian

🛠️ Technology Stack

Core Technologies

Technology Purpose Version
TypeScript Language 5.x
Node.js Runtime 16+
Obsidian API Plugin framework Latest
Handlebars Template engine 4.x
esbuild Build tool Latest
Jest Testing framework 29.x

Key Dependencies

{
  "dependencies": {
    "handlebars": "^4.7.8"
  },
  "devDependencies": {
    "typescript": "5.3.3",
    "obsidian": "github:obsidianmd/obsidian-api",
    "@types/jest": "^29.5.14",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "ts-jest": "^29.4.9",
    "esbuild": "^0.25.4",
    "eslint": "^8.57.1",
    "@typescript-eslint/eslint-plugin": "^7.1.0",
    "@typescript-eslint/parser": "^7.1.0",
    "markdownlint-cli": "^0.45.0"
  }
}

Build Tools


📁 Project Structure

make-it-rain/
├── src/
│   ├── main.ts                 # Plugin entry point
│   ├── types.ts                # TypeScript type definitions
│   ├── settings.ts             # Settings UI component
│   ├── modals.ts               # Bulk fetch and quick import modals
│   ├── template-validator.ts   # Template validation
│   └── utils/
│       ├── apiUtils.ts         # Raindrop.io API client and rate limiting
│       ├── templateUtils.ts    # Template parsing and rendering
│       ├── fileUtils.ts        # File I/O operations
│       ├── formatUtils.ts      # Date, tag, and domain formatting
│       ├── yamlUtils.ts        # YAML frontmatter generation
│       ├── securityUtils.ts    # Content sanitization
│       ├── scrapingUtils.ts    # Archive content extraction
│       └── downloadUtils.ts    # Native Raindrop attachment downloads
├── tests/
│   ├── unit/                   # Unit tests
│   ├── integration/            # Integration tests
│   ├── mocks/                  # Test fixtures and mocks
│   └── setup.ts                # Shared test setup and mocks
├── docs/
│   ├── index.md                # Documentation index
│   ├── user-guide/             # User documentation
│   ├── developer-guide/        # Developer documentation
│   └── release-notes/          # Version history
├── scripts/
│   ├── esbuild.config.mjs      # Build configuration
│   └── copy-to-vault.mjs       # Local deployment helper
├── manifest.json               # Plugin metadata
├── package.json                # npm configuration
├── tsconfig.json               # TypeScript configuration
├── jest.config.js              # Jest configuration
├── .eslintrc.json             # ESLint configuration
└── README.md                   # Project readme

Key Files Explained

File Purpose
main.ts Plugin initialization, command registration, and raindrop processing
settings.ts Settings UI for API token, templates, and plugin preferences
modals.ts UI for bulk import with filtering and quick single-item import
utils/apiUtils.ts Raindrop.io REST API client with rate limiting and retries
utils/templateUtils.ts Nesting-aware AST template parser and renderer
utils/fileUtils.ts Vault file operations (create, write, folders)
utils/formatUtils.ts Date formatting, tag normalization, and domain extraction
utils/yamlUtils.ts Type-coercion-safe YAML frontmatter generation
utils/securityUtils.ts Markdown content sanitization
utils/scrapingUtils.ts Archive content extraction via htmlToMarkdown()
utils/downloadUtils.ts Native Raindrop attachment downloads
types.ts Type definitions for plugin data

🚀 Development Workflow

Setting Up Your Environment

1. Clone and Install

git clone https://github.com/frostmute/make-it-rain.git
cd make-it-rain
npm install

2. Understand the Build

# Development build (watches for changes)
npm run dev

# Production build
npm run build

# Clean build
npm run build:clean

3. Plugin Installation for Testing

# Copy build output to test vault
# Typically: ~/.obsidian/vaults/YOUR_VAULT/.obsidian/plugins/make-it-rain/

# Or use esbuild watch:
npm run dev

Development Commands

# Start development build (watches for changes)
npm run dev

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

# Build for production (includes type check)
npm run build

# Lint markdown documentation
npm run lint:md

Creating a Feature

Step 1: Create a Branch

git checkout -b feature/your-feature-name
git push -u origin feature/your-feature-name

Step 2: Make Your Changes

Step 3: Test Your Changes

# Run tests
npm test

# Test in Obsidian vault
npm run dev
# Then reload plugin in Obsidian

Step 4: Commit and Push

git add .
git commit -m "feat: add new feature description"
git push origin feature/your-feature-name

Step 5: Submit a Pull Request


📋 Code Standards & Style

TypeScript Guidelines

Naming Conventions

// Classes: PascalCase
class RaindropClient { }

// Interfaces: PascalCase, with I prefix (optional)
interface IRaindrop { }
interface FetchOptions { }

// Functions: camelCase
function processRaindrop() { }

// Constants: UPPER_SNAKE_CASE
const MAX_RETRIES = 3;
const API_BASE_URL = "https://api.raindrop.io";

// Variables: camelCase
let currentProgress = 0;
const apiToken = "xxx";

// Private members: _leadingUnderscore
class MyClass {
  private _internalState = 0;
}

// Booleans: is/has prefix
const isLoading = false;
const hasError = true;

Type Annotations

Always include type annotations for clarity:

// ✅ Good
function fetchRaindrops(token: string, limit: number): Promise<Raindrop[]> {
  // ...
}

const config: FetchConfig = {
  token: "xxx",
  limit: 50,
};

// ❌ Avoid
function fetchRaindrops(token, limit) {
  // ...
}

const config = {
  token: "xxx",
  limit: 50,
};

Comments and Documentation

/**
 * Fetches raindrops from Raindrop.io API
 * @param token - API authentication token
 * @param options - Fetch configuration options
 * @returns Promise resolving to array of raindrops
 * @throws {ApiError} If API request fails
 */
function fetchRaindrops(token: string, options: FetchOptions): Promise<Raindrop[]> {
  // Implementation
}

// Use comments for "why", not "what"
// ✅ Good
// Retry with exponential backoff to handle rate limiting
const delay = Math.pow(2, retryCount) * 1000;

// ❌ Avoid
// Add delay
const delay = Math.pow(2, retryCount) * 1000;

Code Organization

Module Structure

// imports
import { Obsidian } from "obsidian";
import { RaindropClient } from "./api/raindropClient";

// types/interfaces
interface ProcessorOptions {
  token: string;
  limit: number;
}

// constants
const DEFAULT_LIMIT = 50;

// class/functions
class RaindropProcessor {
  // constructor
  constructor(private client: RaindropClient) {}

  // public methods
  async process(options: ProcessorOptions): Promise<void> {
    // ...
  }

  // private methods
  private async validate(data: unknown): Promise<void> {
    // ...
  }
}

// exports
export { RaindropProcessor };
export type { ProcessorOptions };

Error Handling

// Define custom error types
class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public originalError?: Error
  ) {
    super(message);
    this.name = "ApiError";
  }
}

// Use try-catch with proper typing
try {
  const result = await fetchRaindrops(token);
  return result;
} catch (error) {
  if (error instanceof ApiError) {
    // Handle API errors
    console.error(`API Error ${error.statusCode}: ${error.message}`);
  } else if (error instanceof Error) {
    // Handle generic errors
    console.error(`Error: ${error.message}`);
  } else {
    // Handle unknown errors
    console.error("Unknown error occurred");
  }
  throw error;
}

Style Rules

Rule Example
Max line length 100 characters (soft), 120 (hard)
Indentation 2 spaces (no tabs)
Semicolons Required at end of statements
Quotes Double quotes (") preferred
Trailing commas Include in multi-line structures
Unused variables Remove or prefix with _
Console logs Use logger utility in production

Formatting Example

// ✅ Good
function processRaindrops(
  raindrops: Raindrop[],
  options: ProcessOptions,
): Promise<ProcessResult> {
  return raindrops
    .filter((r) => shouldInclude(r, options))
    .map((r) => transformRaindrop(r))
    .reduce((acc, r) => merge(acc, r), {});
}

// ❌ Avoid
function processRaindrops(raindrops, options) {
  let result = {}
  for(let i=0; i<raindrops.length; i++){
    if(shouldInclude(raindrops[i],options)){
      result = merge(result, transformRaindrop(raindrops[i]))
    }
  }
  return result
}

✅ Testing Guide

Writing Tests

Test Structure

// tests/unit/processors/raindropProcessor.test.ts

import { RaindropProcessor } from "../../../src/processors/raindropProcessor";
import { mockRaindropClient } from "../../mocks/raindropClient";

describe("RaindropProcessor", () => {
  let processor: RaindropProcessor;

  beforeEach(() => {
    processor = new RaindropProcessor(mockRaindropClient());
  });

  describe("processRaindrop", () => {
    it("should create a note with correct title", async () => {
      const raindrop = {
        id: 123,
        title: "Test Article",
        link: "https://example.com",
      };

      const result = await processor.processRaindrop(raindrop);

      expect(result.title).toBe("Test Article");
    });

    it("should throw error on invalid input", async () => {
      await expect(processor.processRaindrop(null)).rejects.toThrow();
    });
  });
});

Running Tests

# Run all tests
npm test

# Run specific test file
npm test -- raindropProcessor.test.ts

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Coverage Goals

Test Best Practices

  1. Descriptive test names: it("should create note with correct frontmatter when given valid raindrop")
  2. Arrange-Act-Assert pattern: Setup → Execute → Verify
  3. Use mocks for external dependencies
  4. Test edge cases: null, empty, errors
  5. Avoid test interdependencies: Each test should be independent

🐛 Debugging

Development Mode

# Start dev build with watch
npm run dev

# In Obsidian: Settings → Community Plugins → Reload plugin
# (Or reload with Ctrl/Cmd+R in plugin folder)

Viewing Logs

// Use logger utility
import { logger } from "./utils/logger";

logger.info("Processing started", { itemCount: 10 });
logger.error("Failed to fetch", { error });
logger.debug("Details", { data });

Access logs in:

Common Issues

Issue Solution
Changes not reflected Reload plugin (Ctrl/Cmd+R) or restart Obsidian
Type errors Run npm run build to see TypeScript output
Build fails Check npm run build output
Tests fail Review error message, check mock setup

📚 Additional Resources

Within This Project

External Resources


🤝 Contribution Checklist

Before submitting a pull request, ensure:


🔗 Getting Help


📝 Next Steps

  1. Set up your environment: Follow Quick Start
  2. Read the architecture: Review Project Architecture
  3. Pick an issue: Find a good first issue in GitHub
  4. Create a feature branch: Start coding!
  5. Write tests: Ensure quality
  6. Submit a PR: Contribute your changes!

**Ready to contribute?** Start with the [Contributing Guide](../../CONTRIBUTING.md) **Questions?** Check the [FAQ](/make-it-rain/user-guide/faq.html) or [Discussions](https://github.com/frostmute/make-it-rain/discussions)