Initial commit

This commit is contained in:
Forketyfork 2025-06-08 13:58:54 +02:00
commit f8201f0590
No known key found for this signature in database
30 changed files with 4283 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

28
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
yarn install
yarn build
- name: Fail if build modified files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "Build modified files. Commit the output of 'yarn build'."
git status --porcelain
exit 1
fi

44
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
yarn install
yarn build
- name: Fail if build modified files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "Build modified files. Commit the output of 'yarn build'."
git status --porcelain
exit 1
fi
- name: Create plugin archives
run: |
mkdir -p obsidian-food-tracker
cp main.js manifest.json styles.css obsidian-food-tracker/
zip -r obsidian-food-tracker.zip obsidian-food-tracker/
tar -czf obsidian-food-tracker.tar.gz obsidian-food-tracker/
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css obsidian-food-tracker.zip obsidian-food-tracker.tar.gz

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js and styles.css in the repo.
# They should be uploaded to GitHub releases instead.
main.js
styles.css
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# Code coverage dir
coverage/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

7
.prettierignore Normal file
View file

@ -0,0 +1,7 @@
.git
.idea
.vscode
node_modules/
main.js
styles.css
*.json

9
.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"tabWidth": 2,
"useTabs": true,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 120,
"arrowParens": "avoid"
}

1
.yarnrc Normal file
View file

@ -0,0 +1 @@
version-tag-prefix ""

1
AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
CLAUDE.md

43
CLAUDE.md Normal file
View file

@ -0,0 +1,43 @@
# CLAUDE.md
This file provides guidance to AI agents when working with code in this repository.
## Build Commands
- `yarn dev` - Development build
- `yarn dev:watch` - Development build with watch mode
- `yarn prod` - Production build without tests or type checking
- `yarn typecheck` — TypeScript typecheck
- `yarn format` - Format code with Prettier
- `yarn lint` - Check code style with ESLint
- `yarn test` - Run Jest tests
- `yarn test:dev` - Run development build and then tests
- `yarn test:watch` - Run development build and then tests in watch mode
- `yarn build` - Production build (includes tests, typecheck and formatting)
- `yarn build:css` - Minify CSS with CSSO (from styles.src.css to styles.css)
- `yarn version` - Bump version in manifest.json and versions.json
## Typescript & Testing
- Strict null checks required (strictNullChecks: true)
- No implicit any values (noImplicitAny: true)
- Run type check with `yarn typecheck`
- ESLint is configured with typescript-eslint plugin
- Testing is done with Jest (`yarn test:dev`); make sure to always run the build before running the tests (`yarn test:dev` already takes care of that)
- All tests are in the `__tests__` directory
- Test files should end with `.test.ts`
## Code Style
- Avoid useless comments, use them to communicate obscure things and intentions which are not clear from the code rather than obvious details
- Use Obsidian API imports from a single import statement
- Use interfaces for type definitions
- Add explicit error handling with try/catch blocks
- Use async/await for asynchronous operations
- Error messages should be user-friendly
- Avoid unnecessary logging to the console, no debug messages, only errors
- Use consistent indentation (tabs) and spacing
- Class methods order: lifecycle methods first, then functionality
- Any text in UI elements should use "Sentence case" instead of "Title Case"
- Avoid committing changes in `yarn.lock` if you didn't change the `package.json` file, reset the `yarn.lock` file instead
- Avoid committing package-lock.json, since we use yarn; if this file is created as a result of your actions, remove it

42
FoodTrackerPlugin.ts Normal file
View file

@ -0,0 +1,42 @@
import { Plugin } from "obsidian";
import FoodTrackerSettingTab from "./FoodTrackerSettingTab";
interface FoodTrackerPluginSettings {
hello: string;
}
const DEFAULT_SETTINGS: FoodTrackerPluginSettings = {
hello: "world",
};
export default class FoodTrackerPlugin extends Plugin {
settings: FoodTrackerPluginSettings;
async onload() {
await this.loadSettings();
// Add settings tab
this.addSettingTab(new FoodTrackerSettingTab(this.app, this));
// Add an example command
this.addCommand({
id: "food-tracker-example-command",
name: "Food Tracker something...",
callback: () => {
console.error("Test command");
},
});
// Add ribbon icon
this.addRibbonIcon("clipboard-list", "Food Tracker example ribbon action", () => {
console.error("Test ribbon");
});
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, (await this.loadData()) as Partial<FoodTrackerPluginSettings>);
}
async saveSettings() {
await this.saveData(this.settings);
}
}

25
FoodTrackerSettingTab.ts Normal file
View file

@ -0,0 +1,25 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import type FoodTrackerPlugin from "./FoodTrackerPlugin";
export default class FoodTrackerSettingTab extends PluginSettingTab {
plugin: FoodTrackerPlugin;
constructor(app: App, plugin: FoodTrackerPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Hello")
.setDesc("Hello")
.addText(text =>
text.setValue(this.plugin.settings.hello).onChange(async value => {
this.plugin.settings.hello = value;
await this.plugin.saveSettings();
})
);
}
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Forketyfork
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# Food Tracker
[![Build status](https://github.com/forketyfork/obsidian-food-tracker/actions/workflows/build.yml/badge.svg)](https://github.com/forketyfork/obsidian-food-tracker/actions/workflows/build.yml)
An Obsidian plugin to track your food intake (calories, macronutrients) and nutritional information.
## Features
TBD
## Installation
### From Obsidian Community Plugins
1. Open Obsidian
2. Go to Settings > Community plugins
3. Disable Safe mode if necessary
4. Click Browse and search for "Food Tracker"
5. Install the plugin
6. Enable the plugin after installation
### Manual Installation
1. Download the latest release from the GitHub repository
2. Extract the ZIP file into your Obsidian vault's `.obsidian/plugins/` folder
3. Enable the plugin in Obsidian settings
## Usage
TBD
## Requirements
- Obsidian v0.15.0 or higher
## Development
Run the development build with change watch:
```shell
yarn dev:watch
```
Run the TypeScript type check:
```shell
yarn typecheck
```
Run the linter:
```shell
yarn lint
```
Run the tests:
```shell
yarn test
```
Run the tests in watch mode:
```shell
yarn test:watch
```
Generate a coverage report:
```shell
yarn coverage
```
Run the production build (includes tests, type checking, and formatting):
```shell
yarn build
```
Bump the version in `package.json` and `manifest.json`, push the `main` branch,
and publish a new tag:
```shell
yarn release
```
## License
This plugin is licensed under the MIT License.

76
__mocks__/obsidian.js Normal file
View file

@ -0,0 +1,76 @@
// Mock for Obsidian API
module.exports = {
Plugin: class Plugin {
constructor() {}
loadData() {
return Promise.resolve({});
}
saveData() {
return Promise.resolve();
}
addSettingTab() {}
addCommand() {}
addRibbonIcon() {
return {};
}
},
PluginSettingTab: class PluginSettingTab {
constructor() {}
},
Setting: class Setting {
constructor() {
return {
setName: () => this,
setDesc: () => this,
addText: () => this,
addToggle: () => this,
addExtraButton: () => this,
};
}
},
Modal: class Modal {
constructor() {}
open() {}
close() {}
},
App: class App {
constructor() {
this.vault = {
adapter: {
exists: () => Promise.resolve(true),
read: () => Promise.resolve(""),
},
createFolder: () => Promise.resolve(),
create: () => Promise.resolve(),
getAbstractFileByPath: () => ({}),
};
this.workspace = {
getLeaf: () => ({
openFile: () => Promise.resolve(),
}),
};
}
},
TextComponent: class TextComponent {
constructor() {
this.inputEl = {
focus: () => {},
select: () => {},
addEventListener: () => {},
};
return {
setPlaceholder: () => this,
setValue: () => this,
onChange: () => this,
inputEl: this.inputEl,
};
}
},
TFile: class TFile {},
normalizePath: path => path,
requestUrl: () =>
Promise.resolve({
status: 200,
json: {},
}),
};

18
__tests__/plugin.test.ts Normal file
View file

@ -0,0 +1,18 @@
import FoodTrackerPlugin from "../main";
import { App, PluginManifest } from "obsidian";
describe("FoodTrackerPlugin", () => {
let plugin: FoodTrackerPlugin;
beforeEach(async () => {
plugin = new FoodTrackerPlugin({} as App, {} as PluginManifest);
await plugin.onload();
});
test("plugin loads with default settings", async () => {
await plugin.loadSettings();
expect(plugin.settings).toBeDefined();
expect(plugin.settings.hello).toBe("world");
});
});

75
esbuild.config.mjs Normal file
View file

@ -0,0 +1,75 @@
import esbuild from "esbuild";
import builtins from "builtin-modules";
import process from "process";
import fs from "fs";
import { minify } from "csso";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
If you want to view the source, please visit the GitHub repository of this plugin.
*/
`;
const prod = process.argv.includes("--production");
const watch = process.argv.includes("--watch");
const sharedOptions = {
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
keepNames: true,
banner: {
js: banner,
},
};
async function buildJS() {
if (watch) {
const context = await esbuild.context(sharedOptions);
console.log("Watching for changes...");
await context.watch();
} else {
await esbuild.build(sharedOptions);
}
}
function buildCSS() {
const input = fs.readFileSync("styles.src.css", "utf-8");
const output = minify(input).css;
fs.writeFileSync("styles.css", output);
console.log("Minified styles.css");
}
async function main() {
if (prod) {
buildCSS();
}
await buildJS();
}
main().catch(err => {
console.error(err);
process.exit(1);
});

80
eslint.config.mjs Normal file
View file

@ -0,0 +1,80 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
import globals from "globals";
import js from "@eslint/js";
export default [
// Global ignores
{
ignores: [
"**/node_modules/",
"**/main.js",
"**/*.js",
"!eslint.config.mjs",
"!esbuild.config.mjs",
"!jest.config.js",
"!version-bump.mjs",
],
},
// TypeScript files
{
...js.configs.recommended,
files: ["**/*.ts"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: "./tsconfig.json",
ecmaVersion: 2021,
sourceType: "module",
},
globals: {
...globals.node,
...globals.browser,
},
},
plugins: {
"@typescript-eslint": typescriptEslint,
},
rules: {
...typescriptEslint.configs.recommended.rules,
...typescriptEslint.configs["recommended-requiring-type-checking"].rules,
// Disable base rule in favor of TypeScript version
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
// Console restrictions per CLAUDE.md
"no-console": ["error", { allow: ["error"] }],
// Promise handling
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
// Allow empty functions (common in Obsidian plugins)
"@typescript-eslint/no-empty-function": "off",
// Additional type safety rules
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
},
},
// JavaScript/MJS config files
{
files: ["**/*.js", "**/*.mjs"],
languageOptions: {
ecmaVersion: 2021,
sourceType: "module",
globals: {
...globals.node,
},
},
rules: {
"no-unused-vars": ["error", { args: "none" }],
"no-console": "off", // Allow console in config files
},
},
];

BIN
images/fetched.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
images/modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

14
jest.config.js Normal file
View file

@ -0,0 +1,14 @@
const { createDefaultPreset } = require("ts-jest");
const tsJestTransformCfg = createDefaultPreset().transform;
/** @type {import("jest").Config} **/
module.exports = {
testEnvironment: "jsdom",
transform: {
...tsJestTransformCfg,
},
coverageProvider: "v8",
coverageDirectory: "coverage",
collectCoverageFrom: ["**/*.ts", "!**/*.test.ts", "!**/__tests__/**", "!node_modules/**"],
};

1
main.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./FoodTrackerPlugin";

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "food-tracker",
"name": "Food Tracker",
"version": "0.1.0",
"author": "Forketyfork",
"minAppVersion": "0.15.0",
"description": "A plugin to track your food intake (calories, macronutrients) and nutritional information in Obsidian.",
"authorUrl": "https://github.com/forketyfork",
"isDesktopOnly": false
}

57
package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "obsidian-food-tracker",
"version": "0.1.0",
"description": "A plugin to track your food intake (calories, macronutrients) and nutritional information in Obsidian.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"dev:watch": "node esbuild.config.mjs --watch",
"prod": "node esbuild.config.mjs --production",
"typecheck": "tsc -noEmit -skipLibCheck",
"format": "prettier --write .",
"test": "jest",
"test:dev": "yarn dev && jest",
"test:watch": "yarn dev && jest --watch",
"coverage": "yarn dev && jest --coverage",
"lint": "eslint .",
"build": "yarn typecheck && yarn lint && yarn format && yarn prod && yarn test",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"build:css": "csso styles.src.css --output styles.css",
"release": "yarn version && git push origin main && git push origin --tags"
},
"keywords": [
"obsidian",
"plugin",
"food",
"nutrition",
"calories"
],
"author": "Forketyfork",
"license": "MIT",
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.17",
"@types/node": "^22.15.23",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.33.0",
"builtin-modules": "^5.0.0",
"csso-cli": "^4.0.2",
"esbuild": "^0.25.5",
"eslint": "^9.27.0",
"globals": "^16.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^30.0.0-beta.3",
"obsidian": "latest",
"prettier": "^3.5.3",
"ts-jest": "^29.3.4",
"tslib": "^2.8.1",
"tslint": "^6.1.3",
"typescript": "^5.8.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"lodash": "^4.17.21"
}
}

0
styles.src.css Normal file
View file

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"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",
"ES2021"
],
"esModuleInterop": true
},
"include": [
"**/*.ts"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
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 with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "0.15.0"
}

3562
yarn.lock Normal file

File diff suppressed because it is too large Load diff