Storyteller Suite - Build and Testing
Prerequisites
- Node.js: 18.18.0 to 22.x (see
enginesinpackage.json) - npm: Comes with Node.js
# Check versions
node --version # Should be 18.18.0 - 22.x
npm --version
Installation
# Clone repository
git clone https://github.com/maws7140/obsidian-storyteller-suite.git
cd obsidian-storyteller-suite
# Install dependencies
npm install
npm Scripts
| Script | Command | Purpose |
|---|---|---|
dev |
npm run dev |
Development build with watch mode |
build |
npm run build |
Production build |
test |
npm run test |
Run tests once |
test:watch |
npm run test:watch |
Run tests in watch mode |
version |
npm version X.Y.Z |
Bump version |
Development Build
npm run dev
This runs esbuild.config.mjs which:
- Watches for file changes
- Bundles TypeScript to
main.js - Copies CSS to
styles.css - Outputs to project root
esbuild.config.mjs
import esbuild from 'esbuild';
import { builtinModules } from 'module';
const production = process.argv[2] === 'production';
const context = await esbuild.context({
entryPoints: ['src/main.ts'],
bundle: true,
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: production ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
external: [
'obsidian',
'electron',
'@codemirror/*',
...builtinModules.map(m => `node:${m}`),
],
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"'
},
});
if (production) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
Production Build
npm run build
This runs:
- TypeScript type checking (
tsc -p tsconfig.json --noEmit) - esbuild production bundle (
node esbuild.config.mjs production)
Output Files
| File | Description |
|---|---|
main.js |
Bundled plugin code |
styles.css |
Plugin styles |
manifest.json |
Plugin manifest (already exists) |
TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": ["DOM", "ES5", "ES6", "ES7"],
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["**/*.ts"]
}
Testing
Test Framework
Uses Vitest with configuration in vitest.config.ts:
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
resolve: {
alias: {
'obsidian': path.resolve(__dirname, 'test/__mocks__/obsidian.ts'),
},
},
});
Running Tests
# Run all tests once
npm run test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npx vitest run test/utils/DateParsing.test.ts
Test Structure
test/
├── __mocks__/
│ └── obsidian.ts # Mock Obsidian API
├── folders/
│ └── FolderResolver.test.ts
├── utils/
│ ├── DateParsing.test.ts
│ ├── EntityTemplates.test.ts
│ └── YamlSerializer.test.ts
├── yaml/
│ └── EntitySections.test.ts
└── integration/
└── empty-fields-preservation.test.ts
Mock Obsidian API
// test/__mocks__/obsidian.ts
export class App {
vault = new Vault();
workspace = new Workspace();
}
export class Vault {
adapter = {
read: vi.fn(),
write: vi.fn(),
exists: vi.fn(),
};
getAbstractFileByPath = vi.fn();
getMarkdownFiles = vi.fn(() => []);
cachedRead = vi.fn();
modify = vi.fn();
create = vi.fn();
}
export class Modal {
app: App;
contentEl = document.createElement('div');
open = vi.fn();
close = vi.fn();
onOpen = vi.fn();
onClose = vi.fn();
}
// ... other mocks
Example Test
// test/utils/DateParsing.test.ts
import { describe, it, expect } from 'vitest';
import { parseDate, formatDate } from '../../src/utils/DateParsing';
describe('DateParsing', () => {
describe('parseDate', () => {
it('should parse ISO dates', () => {
const result = parseDate('2023-06-15');
expect(result).not.toBeNull();
expect(result?.getFullYear()).toBe(2023);
expect(result?.getMonth()).toBe(5); // 0-indexed
expect(result?.getDate()).toBe(15);
});
it('should parse natural language dates', () => {
const result = parseDate('next Friday');
expect(result).not.toBeNull();
expect(result?.getDay()).toBe(5); // Friday
});
it('should return null for invalid dates', () => {
const result = parseDate('not a date');
expect(result).toBeNull();
});
});
describe('formatDate', () => {
it('should format date to ISO string', () => {
const date = new Date(2023, 5, 15);
const result = formatDate(date);
expect(result).toBe('2023-06-15');
});
});
});
ESLint
Configuration (.eslintrc)
{
"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"
}
}
Running Linter
npx eslint src/ --ext .ts
Version Bumping
Manual Version Update
npm version patch # 1.5.3 → 1.5.4
npm version minor # 1.5.3 → 1.6.0
npm version major # 1.5.3 → 2.0.0
This runs version-bump.mjs which updates:
package.jsonmanifest.jsonversions.json
version-bump.mjs
import { readFileSync, writeFileSync } from 'fs';
const targetVersion = process.env.npm_package_version;
// Update manifest.json
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
let versions = JSON.parse(readFileSync('versions.json', 'utf8'));
versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
Release Workflow
GitHub Actions (.github/workflows/release.yml)
Triggered manually via workflow_dispatch:
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 1.5.2)'
required: true
release_notes:
description: 'Release notes (optional)'
required: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18.x'
- run: npm ci
- run: npm run build
# Update version files
# Create release package
# Create GitHub Release
Release Steps
- Update version files - package.json, manifest.json, versions.json
- Build - Type check and bundle
- Verify - Check main.js exists
- Package - Create zip file
- Tag - Create git tag
- Release - Create GitHub Release with assets
Local Development Setup
Symlink to Obsidian Vault
# Create symlink to test vault
ln -s /path/to/project /path/to/vault/.obsidian/plugins/storyteller-suite
# Or copy files
cp main.js manifest.json styles.css /path/to/vault/.obsidian/plugins/storyteller-suite/
Hot Reload
- Run
npm run dev(watches for changes) - In Obsidian:
Ctrl/Cmd + Rto reload - Or use the Hot Reload plugin
Dependency Management
Renovate Bot
Configured via renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"rangeStrategy": "replace",
"labels": ["dependencies"],
"packageRules": [
{
"matchManagers": ["npm"],
"separateMinorPatch": true
}
]
}
Dependabot
Configured via .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
Debug Testing
test-parsing.js
Simple script to test markdown parsing:
// Run with: node test-parsing.js
function parseSectionsFromMarkdown(content) {
// Section parsing logic
}
const testContent = `---
name: "Test Character"
---
## Description
A brave warrior
## Backstory
Born in a small village...
`;
console.log(parseSectionsFromMarkdown(testContent));
debug-test.js
Additional debugging utilities.
Editor Configuration
.editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
Troubleshooting
Common Issues
Build fails with type errors:
# Check TypeScript errors
npx tsc -p tsconfig.json --noEmit
Tests fail with module not found:
# Ensure mocks are set up
# Check vitest.config.ts alias configuration
Plugin not loading:
- Check
manifest.jsonis valid JSON - Verify
main.jsexists - Check Obsidian console for errors
Styles not applying:
- Verify
styles.cssexists in plugin folder - Check for CSS syntax errors