diff --git a/package.json b/package.json index 6a00766..584d18b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "esbuild": "0.17.3", "obsidian": "latest", "tslib": "2.4.0", - "typescript": "4.7.4" + "typescript": "4.7.4", + "commander": "13.1.0" } } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..adf2988 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,33 @@ +## Installation and Usage + +1. Install dependencies: +```bash + npm install commander +``` +2. Install tsx for executing TypeScript directly: +```bash + npm install -g tsx +``` +3. Run the script: +```bash + tsx generate-test-vault.ts --count 40000 --output ./test-vault +``` + +## Script Options + +- `--count` or `-c`: Number of notes to generate (default: 40000) +- `--output` or `-o`: Output directory path (default: ./test-vault) +- `--tag-prefix` or `-t`: Status tag prefix (default: obsidian-note-status) +- `--depth` or `-d`: Maximum folder depth (default: 5) +- `--max-per-folder` or `-m`: Maximum files per folder (default: 200) + +## Performance Considerations +The script creates a realistic vault structure with: + +- Multiple folder levels +- Random distribution of notes across folders +- Variety of statuses including single and multiple status notes +- Varied note content length and structure +- Proper frontmatter format matching your plugin's expectations + +This will let you test real-world performance issues with your plugin without needing to manually create thousands of notes. \ No newline at end of file diff --git a/scripts/generate-test-vault.ts b/scripts/generate-test-vault.ts new file mode 100644 index 0000000..cf7abbc --- /dev/null +++ b/scripts/generate-test-vault.ts @@ -0,0 +1,243 @@ +#!/usr/bin/env tsx +/** + * Obsidian Test Vault Generator + * + * This script generates a large number of markdown files with status tags + * to test the performance of the Note Status plugin with large vaults. + * + * Usage: + * npx tsx generate-test-vault.ts --count 40000 --output ./test-vault + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { program } from 'commander'; + +// Define command line options +program + .option('-c, --count ', 'Number of notes to generate', '40000') + .option('-o, --output ', 'Output directory path', './test-vault') + .option('-t, --tag-prefix ', 'Status tag prefix', 'obsidian-note-status') + .option('-d, --depth ', 'Maximum folder depth', '5') + .option('-m, --max-per-folder ', 'Maximum files per folder', '200') + .parse(process.argv); + +const options = program.opts(); + +// Validate options +const noteCount = parseInt(options.count, 10); +const outputDir = options.output; +const tagPrefix = options.tagPrefix; +const maxDepth = parseInt(options.depth, 10); +const maxPerFolder = parseInt(options.maxPerFolder, 10); + +if (isNaN(noteCount) || noteCount <= 0) { + console.error('Error: Note count must be a positive number'); + process.exit(1); +} + +// Define possible statuses (matching your plugin's defaults) +const statuses = [ + 'active', + 'onHold', + 'completed', + 'dropped', + 'unknown', + // Add some from the colorful template + 'idea', + 'draft', + 'inProgress', + 'editing', + 'pending' +]; + +// Define topics for more realistic note titles +const topics = [ + 'Project', 'Meeting', 'Research', 'Idea', 'Book', 'Journal', + 'Task', 'Recipe', 'Person', 'Place', 'Event', 'Course', + 'Paper', 'Review', 'Analysis', 'Summary', 'Plan', 'Design', + 'Budget', 'Report', 'Feature', 'Bug', 'Release', 'Backlog', + 'Sprint', 'Goal', 'OKR', 'KPI', 'Metric', 'Reflection' +]; + +// Generate a random status +function getRandomStatus(): string { + const multipleStatuses = Math.random() > 0.8; // 20% chance of multiple statuses + + if (multipleStatuses) { + const count = Math.floor(Math.random() * 2) + 2; // 2-3 statuses + const selectedStatuses = new Set(); + + while (selectedStatuses.size < count) { + selectedStatuses.add(statuses[Math.floor(Math.random() * statuses.length)]); + } + + return JSON.stringify(Array.from(selectedStatuses)); + } else { + return JSON.stringify([statuses[Math.floor(Math.random() * statuses.length)]]); + } +} + +// Generate a random title +function generateTitle(): string { + const topic = topics[Math.floor(Math.random() * topics.length)]; + const qualifier = Math.random() > 0.5 ? + ['New', 'Important', 'Urgent', 'Weekly', 'Monthly', 'Daily', 'Annual'][Math.floor(Math.random() * 7)] : ''; + const number = Math.random() > 0.3 ? Math.floor(Math.random() * 1000).toString() : ''; + + return [qualifier, topic, number].filter(Boolean).join(' ').trim(); +} + +// Generate note content with random length +function generateContent(): string { + const paragraphCount = Math.floor(Math.random() * 5) + 1; + let content = ''; + + for (let i = 0; i < paragraphCount; i++) { + const sentenceCount = Math.floor(Math.random() * 5) + 1; + let paragraph = ''; + + for (let j = 0; j < sentenceCount; j++) { + const wordCount = Math.floor(Math.random() * 15) + 5; + const sentence = Array(wordCount).fill('lorem').join(' ') + '. '; + paragraph += sentence; + } + + content += paragraph + '\n\n'; + } + + return content; +} + +// Create a note with frontmatter containing status +function createNote(title: string, folderPath: string): void { + const safeName = title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, ' '); + const fileName = `${safeName}.md`; + const filePath = path.join(folderPath, fileName); + + // Create frontmatter with random status + const status = getRandomStatus(); + const frontmatter = `---\n${tagPrefix}: ${status}\n---\n\n`; + + // Create content + const content = `# ${title}\n\n${generateContent()}`; + + // Write file + fs.writeFileSync(filePath, frontmatter + content); +} + +// Generate a folder structure and notes +function generateNotes(count: number, baseDir: string): void { + // Create base directory if it doesn't exist + if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir, { recursive: true }); + } + + // Track created files + let created = 0; + let foldersCreated = 0; + + // Create a function to generate notes in a folder with recursion + function generateNotesInFolder(folderPath: string, depth: number): void { + if (created >= count) return; + + // Determine how many files to create in this folder + const filesInThisFolder = Math.min( + Math.floor(Math.random() * maxPerFolder) + 1, + count - created + ); + + // Create files in this folder + for (let i = 0; i < filesInThisFolder; i++) { + if (created >= count) break; + + const title = generateTitle(); + createNote(title, folderPath); + created++; + + // Display progress + if (created % 1000 === 0 || created === count) { + console.log(`Created ${created}/${count} notes...`); + } + } + + // Possibly create subfolders if not at max depth + if (depth < maxDepth && created < count) { + // Decide how many subfolders to create + const subfoldersCount = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < subfoldersCount; i++) { + if (created >= count) break; + + const folderName = `Folder ${++foldersCreated}`; + const subfolderPath = path.join(folderPath, folderName); + + // Create subfolder + if (!fs.existsSync(subfolderPath)) { + fs.mkdirSync(subfolderPath); + } + + // Recursively generate notes in subfolder + generateNotesInFolder(subfolderPath, depth + 1); + } + } + } + + // Start generation + generateNotesInFolder(baseDir, 0); + + console.log(`\nSuccessfully created ${created} notes in ${foldersCreated} folders.`); + console.log(`Output directory: ${path.resolve(baseDir)}`); +} + +// Create .obsidian folder with minimal config to ensure it's recognized as a vault +function createObsidianConfig(baseDir: string): void { + const obsidianDir = path.join(baseDir, '.obsidian'); + if (!fs.existsSync(obsidianDir)) { + fs.mkdirSync(obsidianDir, { recursive: true }); + } + + // Create a minimal config.json + const configPath = path.join(obsidianDir, 'config.json'); + const config = { + "baseFontSize": 16, + "pluginEnabledStatus": { + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "page-preview": true, + "templates": true + } + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + // Create a minimal appearance.json + const appearancePath = path.join(obsidianDir, 'appearance.json'); + const appearance = { + "baseFontSize": 16, + "theme": "obsidian" + }; + + fs.writeFileSync(appearancePath, JSON.stringify(appearance, null, 2)); +} + +// Main execution +console.log(`Generating ${noteCount} notes in ${outputDir} with tag prefix '${tagPrefix}'`); +console.time('Generation completed in'); + +// Create test vault +generateNotes(noteCount, outputDir); + +// Create Obsidian configuration +createObsidianConfig(outputDir); + +console.timeEnd('Generation completed in'); +console.log('\nInstructions:'); +console.log('1. Open Obsidian'); +console.log('2. Select "Open folder as vault"'); +console.log(`3. Navigate to: ${path.resolve(outputDir)}`); +console.log('4. Install and enable your Note Status plugin'); +console.log('5. Test the plugin performance with this large vault'); \ No newline at end of file