Welcome back to the second part! In the first part, we explored the basics of creating an Obsidian plugin. If you missed it, I recommend reading it first. Now we'll improve the overall architecture and file layout of the plugin as well as adding basic error handling. Furthermore, I share my learnings from releasing the first patch versions, lets get started!
The Starting Point
Starting from the mentioned plugin-template, the overall structure of the plugin are a bunch of files all within the root folder of the repository:
main.ts
styles.css
manifest.json
versions.js
While not displaying technical configurations files such as package.json
. The plugins's entire source code, including its settings, UI and logic is contained in a single file, main.ts
. While serving the point of a starting point and template, this does not represent a good structure for further feature development.
The Desired State
Before describing the current layout and architecture, let's first recap some general guidelines used in this process:
- Start simple and improve / reconcile as you grow: You don't win anything, if you start your simple project with the ultimate, scalable architecture. Start simple with your current requirements while being open for changes.
- Separate different tasks into different files. We want to get rid of a single
main.ts
, that is responsible for all aspects of the plugin. With that done, I first started to separate all the classes within themain.ts
into their own files and moved all of them into asrc
directory yielding:
src
| commands.ts
| gemini-client.ts
| main.ts
| settings-tab.ts
| settings.ts
package.json
package-lock.jsoon
README.md
The main idea behind this structure is the separation of different tasks of the plugin into their own files: settings-tab.ts
and settings.ts
defines the UI and data structures for the required API-Key settings.
The single command of this plugin (with room to add more) is defined in commands.ts
and gemini-client.ts
abstracts the official Gemini library for the specific use case of this plugin and is used by the command. All parts are wired together in main.ts
which bootstraps the plugin within the onload
function:
export default class GeminiGenerator extends Plugin {
settings: GeminiGeneratorSettings;
// Loads the plugin.
async onload() {
await this.loadSettings();
this.addSettingTab(new GeminiGeneratorSettingTab(this.app, this));
this.addEditorCommands();
}
private addEditorCommands() {
for (const command of getEditorCommands(this)) {
this.addCommand(command);
}
}
...
}
In contrast to the initial version, the main.ts
got rid of all dependencies of the command. It only handles the settings and registers all EditorCommands (line 10). This registration is implemented with a combination of factory functions and dependency injection.
Quick recap, EditorCommands are registered in Obsidian though the call of addCommand
, providing an object with the commands configuration and the callback, which represents the commands implementation:
this.addCommand(
{ id: 'print-greeting-to-console',
name: 'Print greeting to console',
callback: () => { console.log('Hey, you!');
},
});
Hence, we are only able to register one command at a time. This is the reason, the private method addEditorCommands
of main.ts
iterates over a list of commands, returned by getEditorCommands
and registers them one-by-one. As the commands implementation require a reference to the plugin respectively editor, this
is passed as an argument.
The implementation is rather simple: The method simply returns a list of all (currently one) commands which are created through their corresponding factory function, which intern construct and return the mentioned command object required by the Obsidian API.
export const getEditorCommands = (plugin : GeminiGenerator) =>
[
buildGenerateNoteCommand(plugin)
];
With the addition of more commands, it could prove useful to create a new folder Commands
, where each command is defined within its own file.
All in all, this structure offers several advantages:
- Clearer separation of concerns
- Improved scalability for future features
- Easier maintenance # Basic Error Handling The current error handling is minimal at most, but this series is about sharing my leanings, so here we go! The main error handling can be described by four lines:
if(!result){
notice.setMessage("❌ An error occured during the Google Gemini Request")
return;
}
Basically, if any error occurs during the request to Google Gemini, a small error toast is shown. This served well for the beginning, yet this is not very precise for the user, as some prompts would benefit from a more differentiated error message.
Hence, I would suggest you to think about feasible error cases and messages. Do not overstrain your users with technical messages, while displaying enough information enabling to distinguish an input error from a server error for example.
Learnings from Releasing the first Versions
I use a small GitHub Action for bundling an creating a GitHub Release whenever I a git tag is pushed / created. The GitHub release is created as a draft, such that I'm able to adapt the release log or title. If you are using this setup as well, do not forget to publish this release, as otherwise no one will be able to update / download your plugin anymore. That's because in you repository, the versions.json
already contains you new version but as the GitHub release is still a draft, no one is able to download it.
If you have the same setup as I do, I would recommend following this procedure:
- Push a git tag for a new version
- Wait for the Action to finish
- Validate the created release draft
- Publish the release
Again, thanks for reading and following this small series. As always, if you have suggestions or feedback, I'd love to hear from you in the comments! I'm looking forward to having more time for further development of the plugin. In the next part, we will add some more settings and another command, so stay tuned!
Further Resources and Tips
- Take a look at other Obsidian plugins such as Obsidian Linter, they are all open source!
- Start simple, be open to change and adapt as you go
- Sourcecode of this plugin
Top comments (0)