Initial commit

This commit is contained in:
Lisandra 2024-12-17 16:24:45 +01:00 committed by GitHub
commit 05464c396a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1387 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

104
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

@ -0,0 +1,104 @@
name: "Bug report"
description: Fill a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report.
- type: checkboxes
attributes:
label: Issue validation
description: |
- Thanks to check if your issue is relative to the repository. Any non relative or duplicate issue will be closed.
- Please, check the documentation and the configuration files before submitting your request.
- Issue not in English will be closed.
options:
- label: "I checked the issue to prevent duplicate"
required: true
- label: "I checked my configurations files and the documentation"
required: true
- type: textarea
id: describe-bug
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: "Tell us what you see! And don't forget the error"
validations:
required: true
- type: textarea
id: repro-bug
attributes:
label: How to reproduce ?
description: Step to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
id: minimal-repro
attributes:
label: Minimal Reproducible Example
description: Please provide a minimal reproducible example.
validations:
required: true
- type: textarea
attributes:
label: Configuration
description: |
Open the configuration settings with any text editor. The settings are located in `.obsidian/plugins/<%= data.id %>`
render: JSON
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. You can open the Obsidian's console with "CTRL+MAJ+I."
render: bash session
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: markdown
attributes:
value: |
## Environment
Please fill out the following information about your environment. If you are unsure about any of them, just leave it blank.
- type: dropdown
id: version
attributes:
label: OS
description: Check your OS
multiple: true
options:
- IOS
- Android
- MacOS
- Windows
- Linux
- type: textarea
attributes:
label: Obsidian information
description: |
Please copy and paste the information about your Obsidian version using the command "show debug info" in the obsidian's commands palette.
render: bash session
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin version
description: Please copy and paste the version of the plugin you are using.
placeholder: "1.0.0"
validations:
required: true

View file

@ -0,0 +1,80 @@
name: "Feature request"
description: "Suggest an idea for this project"
title: "[FR]: "
labels: ["enhancement"]
assignees:
- Mara-Li
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this Feature request!
- type: checkboxes
attributes:
label: Issue validation
description: |
- Thanks to check if your issue is relative to the repository. Any non relative or duplicate issue will be closed.
- Please, check the documentation and the configuration files before submitting your request.
- Issue not in English will be closed.
options:
- label: "I checked the issue to prevent duplicate"
required: true
- label: "I checked my configurations files and the documentation"
required: true
- type: textarea
id: describe-request
attributes:
label: Is your feature related to a problem ?
description: If you found a solution with the inherent limit I had with Obsidian, please, add it here!
placeholder: "Tell me the original problem"
- type: textarea
id: describe-solution
attributes:
label: What solution do you want to see ?
description: Describe your idea here!
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Describe the alternative you've considered
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: markdown
attributes:
value: |
## Environment
Please fill out the following information about your environment. If you are unsure about any of them, just leave it blank.
- type: dropdown
id: version
attributes:
label: OS
description: Check your OS
multiple: true
options:
- IOS
- Android
- MacOS
- Windows
- Linux
- type: textarea
attributes:
label: Obsidian information
description: |
Please copy and paste the information about your Obsidian version using the command "show debug info" in the obsidian's commands palette.
render: bash session
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin version
description: Please copy and paste the version of the plugin you are using.
placeholder: "1.0.0"
validations:
required: true

16
.github/workflows/auto_assign.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Auto Assign
on:
issues:
types: [opened, edited, labeled, unlabeled]
pull_request:
types: [opened, edited, labeled, unlabeled]
jobs:
auto-assign:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/auto-assign@v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
reviewers: ${{github.repository_owner}}
assignees: ${{github.repository_owner}}
skipKeywords: wip, draft

41
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Release obsidian plugin
on:
workflow_dispatch:
inputs:
bump:
default: false
description: "Bump version based on semantic release"
type: boolean
required: false
beta:
default: false
description: "Make a beta release"
type: boolean
required: false
push:
tags:
- "*"
permissions:
contents: write
jobs:
release:
if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && !inputs.bump)
uses: mara-li/reusable-workflows/.github/workflows/obsidian-plugin-release.yaml@main
with:
PLUGIN_NAME: <%= data.id %>
CACHE: "<%= data.packageManager %>"
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bump-version-and-release:
if: ${{ inputs.bump }}
uses: mara-li/reusable-workflows/.github/workflows/obsidian-plugin-bump-version.yaml@main
with:
PLUGIN_NAME: <%= data.id %>
BETA: ${{ inputs.beta }}
CACHE: "<%= data.packageManager %>"
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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 file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
styles.css
!src/styles.css
# Exclude sourcemaps
*.map
# obsidian
data.json
.hotreload
# Exclude macOS Finder (System Explorer) View States
.DS_Store
.env

3
.npmrc Normal file
View file

@ -0,0 +1,3 @@
tag-version-prefix=""
auto-install-peers=true
enable-pre-post-scripts=true

33
README.md Normal file
View file

@ -0,0 +1,33 @@
# <%= data.name %>
<%= data.description %>
## ⚙️ Usage
## 📥 Installation
- [ ] From Obsidian's community plugins
- [x] Using BRAT with `https://github.com/<%= data.author.name%>/<% data.id%>`
- [x] From the release page:
- Download the latest release
- Unzip `<%= data.id %>.zip` in `.obsidian/plugins/` path
- In Obsidian settings, reload the plugin
- Enable the plugin
### 🎼 Languages
- [x] English
- [ ] French
To add a translation:
1. Fork the repository
2. Add the translation in the `src/i18n/locales` folder with the name of the language (ex: `fr.json`).
- You can get your locale language from Obsidian using [obsidian translation](https://github.com/obsidianmd/obsidian-translations) or using the commands (in templater for example) : `{{TEMPLATE_PLACEHOLDER LOCALE}}`
- Copy the content of the [`en.json`](./src/i18n/locales/en.json) file in the new file
- Translate the content
3. Edit `i18n/i18next.ts` :
- Add `import * as <lang> from "./locales/<lang>.json";`
- Edit the `ressource` part with adding : `<lang> : {translation: <lang>}`

130
biome.json Normal file
View file

@ -0,0 +1,130 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"complexity": {
"noExtraBooleanCast": "error",
"noMultipleSpacesInRegularExpressionLiterals": "error",
"noUselessCatch": "error",
"noWith": "error"
},
"style": {
"noVar": "error",
"useFilenamingConvention": "error",
"useImportType": "error",
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false
}
},
"useTemplate": "warn",
"useConst": "error"
},
"correctness": {
"noConstAssign": "error",
"noConstantCondition": "error",
"noEmptyCharacterClassInRegex": "error",
"noEmptyPattern": "error",
"noGlobalObjectCalls": "error",
"noInnerDeclarations": "error",
"noInvalidConstructorSuper": "error",
"noNewSymbol": "error",
"noNonoctalDecimalEscape": "error",
"noPrecisionLoss": "error",
"noSelfAssign": "error",
"noSetterReturn": "error",
"noSwitchDeclarations": "error",
"noUndeclaredVariables": "error",
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error",
"noUnusedVariables": "warn",
"noUnusedImports": "warn",
"useIsNan": "error",
"useValidForDirection": "error",
"useYield": "error"
},
"suspicious": {
"noAssignInExpressions": "error",
"noAsyncPromiseExecutor": "error",
"noCatchAssign": "error",
"noClassAssign": "error",
"noCompareNegZero": "error",
"noControlCharactersInRegex": "error",
"noDebugger": "error",
"noDuplicateCase": "error",
"noDuplicateClassMembers": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noEmptyBlockStatements": "error",
"noFallthroughSwitchClause": "error",
"noFunctionAssign": "error",
"noGlobalAssign": "error",
"noImportAssign": "error",
"noMisleadingCharacterClass": "error",
"noPrototypeBuiltins": "error",
"noRedeclare": "error",
"noShadowRestrictedNames": "error",
"noUnsafeNegation": "error",
"useGetterReturn": "error",
"useValidTypeof": "error"
},
"nursery": {
"noDuplicateJsonKeys": "error"
}
},
"ignore": [
"**/npm node_modules",
"**/build",
"**/dist",
"**/src/i18n/locales"
]
},
"overrides": [
{
"ignore": [
"**/*.js"
],
"include": [
"**/*.ts",
"**/*.tsx"
]
},
{
"include": [
"**/*.js"
]
},
{
"include": [
"*.json"
]
}
],
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 90
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "es5"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
}
}

129
commit-and-tag-version.mjs Normal file
View file

@ -0,0 +1,129 @@
import { Command, Option } from "commander";
import commitAndTagVersion from "commit-and-tag-version";
import dedent from "dedent";
import pkg from "ansi-colors";
const { red, dim, gray, italic, bold, cyan, blue, green, underline, yellow, theme } = pkg;
const program = new Command();
theme({
danger: red,
dark: dim.gray,
disabled: gray,
em: italic,
heading: bold.underline,
info: cyan,
muted: dim,
primary: blue,
strong: bold,
success: green.bold,
warning: yellow.underline,
});
const info = (msg) => pkg.info(msg);
const heading = (msg) => pkg.heading(msg);
const em = (msg) => pkg.em(msg);
program
.description("Bump version and create a new tag")
.option("-b, --beta", "Pre-release version")
.option("--dry-run", "Dry run")
.addOption(
new Option("-r, --release-as <size>", "release type version").choices([
"major",
"minor",
"patch",
])
);
program.parse();
const opt = program.opts();
const betaMsg = opt.beta ? em("- Pre-release\n\t") : "";
const dryRunMsg = opt.dryRun ? em("- Dry run\n\t") : "";
const releaseAsMsg = opt.releaseAs ? em(`- Release as ${underline(opt.releaseAs)}`) : "";
const msg = dedent(`
${heading("Options :")}
${betaMsg}${dryRunMsg}${releaseAsMsg}
`);
console.log(msg);
console.log();
if (opt.beta) {
console.log(`${bold.green(">")} ${info(underline("Bumping beta version..."))}`);
console.log();
const bumpFiles = [
{
filename: "manifest-beta.json",
type: "json",
},
{
filename: "package.json",
type: "json",
},
{
filename: "package-lock.json",
type: "json",
},
];
commitAndTagVersion({
infile: "CHANGELOG-beta.md",
bumpFiles,
prerelease: "",
dryRun: opt.dryRun,
tagPrefix: "",
scripts: {
postchangelog: "node hooks/_changelog.mjs -b",
},
})
.then(() => {
console.log("Done");
})
.catch((err) => {
console.error(err);
});
} else {
const versionBumped = opt.releaseAs
? info(`Release as ${underline(opt.releaseAs)}`)
: info("Release");
console.log(`${bold.green(">")} ${underline(versionBumped)}`);
console.log();
const bumpFiles = [
{
filename: "manifest-beta.json",
type: "json",
},
{
filename: "package.json",
type: "json",
},
{
filename: "package-lock.json",
type: "json",
},
{
filename: "manifest.json",
type: "json",
},
];
commitAndTagVersion({
infile: "CHANGELOG.md",
bumpFiles,
dryRun: opt.dryRun,
tagPrefix: "",
releaseAs: opt.releaseAs,
scripts: {
postchangelog: "node hooks/_changelog.mjs",
},
})
.then(() => {
console.log("Done");
})
.catch((err) => {
console.error(err);
});
}

185
esbuild.config.mjs Normal file
View file

@ -0,0 +1,185 @@
import * as fs from "fs";
import * as path from "path";
import builtins from "builtin-modules";
import { Command } from "commander";
import dotenv from "dotenv";
import esbuild from "esbuild";
import manifest from "./manifest.json" with { type: "json" };
import packageJson from "./package.json" with { type: "json" };
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: ${packageJson.repository}
*/
`;
function cleanOutDir(outdir) {
if (fs.existsSync(outdir)) {
fs.rm(outdir, { recursive: true });
}
}
/**
* @typedef Options
* @prop {boolean|undefined} production
* @prop {string|boolean|undefined} vault
* @prop {string|undefined} outputDir
* @prop {boolean|undefined} beta
*/
dotenv.config({ path: [".env"] });
const program = new Command();
program
.option("-p, --production", "Production build")
.option("-v, --vault [vault]", "Use vault path")
.action((v) => {
if (!v) return false;
if (typeof v === "string") return path.resolve(v.replace(/\\/g, "/"));
return process.env.VAULT;
})
.option("-o, --output-dir <path>", "Output path")
.option("-b, --beta", "Pre-release version")
.parse();
program.parse();
/** OPTIONS */
/** @type {Options} */
const opt = program.opts();
/** @type {boolean} */
const prod = opt.production ?? false;
/** VARIABLES **/
const isStyled = fs.existsSync("src/styles.css");
const pluginID = manifest.id;
/** FOLDER PATHS **/
const folderPlugin = opt.vault
? path.join(opt.vault, ".obsidian", "plugins", pluginID)
: undefined;
if (folderPlugin && !fs.existsSync(folderPlugin)) {
fs.mkdirSync(folderPlugin, { recursive: true });
}
if (opt.beta && !fs.existsSync("manifest-beta.json")) {
fs.copyFileSync("manifest.json", "manifest-beta.json");
}
let outDir = "./";
if (opt.outputDir) {
outDir = opt.outputDir;
cleanOutDir(outDir);
} else if (opt.vault) {
outDir = folderPlugin;
if (!prod) fs.writeFileSync(path.join(folderPlugin, ".hotreload"), "");
} else if (prod) {
outDir = "./dist";
//clean dist if
cleanOutDir(outDir);
}
/**
* Move styles.css to output directory
*/
const moveStyles = {
name: "move-styles",
setup(build) {
build.onEnd(() => {
fs.copyFileSync("src/styles.css", "./styles.css");
});
},
};
/**
* Export to vault if set in environment variable
*/
const exportToVaultFunc = {
name: "export-to-vault",
setup(build) {
build.onEnd(() => {
if (!folderPlugin)
throw new Error("VAULT environment variable not set, skipping export to vault");
fs.copyFileSync(`${outDir}/main.js`, path.join(folderPlugin, "main.js"));
if (fs.existsSync(`${outDir}/styles.css`))
fs.copyFileSync("./styles.css", path.join(folderPlugin, "styles.css"));
if (opt.beta)
fs.copyFileSync("manifest-beta.json", path.join(folderPlugin, "manifest.json"));
else fs.copyFileSync("./manifest.json", path.join(folderPlugin, "manifest.json"));
});
},
};
/**
* Export to production folder
*/
const exportToDist = {
name: "export-to-dist",
setup(build) {
build.onEnd(() => {
if (opt.beta)
fs.copyFileSync("manifest-beta.json", path.join(outDir, "manifest.json"));
else fs.copyFileSync("manifest.json", path.join(outDir, "manifest.json"));
});
},
};
/**
* ENTRIES *
*/
const entryPoints = ["src/main.ts"];
if (isStyled) entryPoints.push("src/styles.css");
/** PLUGINS **/
const plugins = [];
if (isStyled) plugins.push(moveStyles);
if (prod) plugins.push(exportToDist);
if (prod && opt.vault) plugins.push(exportToVaultFunc);
/**
* BUILD
*/
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints,
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: "esnext",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
minifySyntax: prod,
minifyWhitespace: prod,
outdir: outDir,
plugins,
});
if (prod) {
console.log("🎉 Build for production");
console.log(`📤 Output directory: ${outDir}`);
await context.rebuild();
console.log("✅ Build successful");
process.exit(0);
} else {
console.log("🚀 Start development build");
console.log(`📤 Output directory: ${outDir}`);
await context.watch();
}

302
generate.mjs Normal file
View file

@ -0,0 +1,302 @@
import c from "ansi-colors";
import ejs from "ejs";
import fs from "node:fs";
import path from "node:path";
import prompts from "prompts";
import licenses from "spdx-license-list/full.js";
import { getLicense } from "license";
import packageJson from "./package.json" assert { type: "json" };
const { dim, reset } = c;
import { execa } from "execa";
import gitUserName from "git-user-name";
c.theme({
danger: c.red,
dark: c.dim.gray,
disabled: c.gray,
em: c.italic,
heading: c.bold.underline,
info: c.cyan,
muted: c.dim,
primary: c.blue,
strong: c.bold,
success: c.green.bold,
underline: c.underline,
warning: c.yellow.underline,
});
const capitalize = (s) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
};
/**
* Render ejs file
* @param {fs.Dirent} file
*/
function ejsRender(file, data) {
const pathFiles = path.join(file.path, file.name);
const template = fs.readFileSync(pathFiles, { encoding: "utf-8" });
const processedTemplate = ejs.render(template, { data });
fs.writeFileSync(pathFiles, processedTemplate, { encoding: "utf-8" });
}
function updateManifest(data, answer) {
const manifest = fs.readFileSync("./manifest.json", { encoding: "utf-8" });
const processedManifest = ejs.render(manifest, { data });
fs.writeFileSync("manifest.json", processedManifest, { encoding: "utf-8" });
const license = getLicense(answer.license, {
author: data.author.name,
year: new Date().getFullYear(),
});
fs.writeFileSync("LICENSE", license, { encoding: "utf-8" });
fs.writeFileSync("manifest-beta.json", processedManifest, {
encoding: "utf-8",
});
}
/**
* Get package manager
* @returns {"pnpm" | "yarn" | "npm" | "bun" | undefined}
*/
function getPackageManager() {
if (fs.existsSync("yarn.lock")) return "yarn";
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
if (fs.existsSync("package-lock.json")) return "npm";
if (fs.existsSync("bun.lockb")) return "bun";
return undefined;
}
function processReadme(data) {
const readme = fs.readFileSync("./README.md", { encoding: "utf-8" });
const processedReadme = ejs.render(readme, { data });
fs.writeFileSync(
"README.md",
processedReadme.replace(
"{{TEMPLATE_PLACEHOLDER LOCALE}}",
"<% tp.obsidian.moment.locale() %>"
),
{ encoding: "utf-8" }
);
}
function processCi(data) {
const ci = fs.readFileSync("./.github/workflows/ci.yaml", {
encoding: "utf-8",
});
const processedCi = ejs.render(ci, { data });
fs.writeFileSync(".github/workflows/ci.yaml", processedCi, {
encoding: "utf-8",
});
}
function processBugReport(data) {
const path = fs.readFileSync("./.github/ISSUE_TEMPLATE/bug.yml", {
encoding: "utf-8",
});
const processedCi = ejs.render(path, { data });
fs.writeFileSync("./.github/ISSUE_TEMPLATE/bug.yml", processedCi, {
encoding: "utf-8",
});
}
function updatePackageJson(data, answer) {
packageJson.author = data.author.name;
packageJson.name = data.id;
packageJson.license = answer.license;
packageJson.description = data.description;
delete packageJson.scripts.generate;
delete packageJson.devDependencies["@types/ejs"];
delete packageJson.dependencies.ejs;
delete packageJson.dependencies.prompts;
delete packageJson.dependencies["spdx-license-list"];
delete packageJson.dependencies.execa;
delete packageJson.dependencies.license;
delete packageJson.dependencies["git-user-name"];
fs.writeFileSync("package.json", JSON.stringify(packageJson, null, 2), {
encoding: "utf-8",
});
}
/**
* Install dep based on package manager detected (using lockfile)
* @param {"pnpm" | "npm" | "yarn" | "bun" | undefined} packageManager
*/
async function updateDep(packageManager) {
switch (packageManager) {
case "yarn":
console.log(c.info("Detected yarn, running yarn install"));
//replace pnpm with yarn in package.json scripts
fs.writeFileSync(
"package.json",
fs.readFileSync("package.json", "utf-8").replace("pnpm", "yarn"),
"utf-8"
);
await execa("yarn", ["install"]);
break;
case "npm":
console.log(c.info("Detected npm, running npm install"));
fs.writeFileSync(
"package.json",
fs.readFileSync("package.json", "utf-8").replace("pnpm", "npm"),
"utf-8"
);
await execa("npm", ["install"]);
break;
case "pnpm":
console.log(c.info("Detected pnpm, running pnpm install"));
await execa("pnpm", ["install"]);
break;
case "bun":
console.log(c.info("Detected bun, running bun install"));
await execa("bun", ["install"]);
fs.writeFileSync(
"package.json",
fs.readFileSync("package.json", "utf-8").replace("pnpm", "bun"),
"utf-8"
);
break;
default:
throw new Error("No package manager detected, please run yarn/npm/pnpm install");
}
console.log(c.success("✅ Installed dependencies"));
}
const defaultPluginID = process
.cwd()
.split(path.sep)
.pop()
.toLowerCase()
.replaceAll(" ", "-")
.replace(/-?obsidian-?/, "");
const answer = await prompts(
[
{
type: () => "text",
name: "id",
message: `Enter the plugin ID ${reset("(lowercase, no spaces)")}`,
initial: defaultPluginID,
format: (value) => value.toLowerCase().replaceAll(" ", "-").toLowerCase(),
validate: (value) => (value.length > 0 ? true : "Please enter a valid plugin ID"),
},
{
type: "text",
name: "name",
message: "Enter the plugin name",
initial: (prev) =>
prev
.replace("obsidian-plugin", "")
.split("-")
.filter((word) => word.length > 0)
.map((word) => capitalize(word))
.join(" "),
},
{
type: "text",
name: "description",
message: "Enter the plugin description",
},
{
type: "text",
name: "author",
message: "Enter the author name",
initial: gitUserName(),
},
{
type: "text",
name: "authorUrl",
message: "Enter the author URL",
initial: (prev) => `https//github.com/${prev}`,
},
{
type: "confirm",
name: "desktopOnly",
message: "Is this plugin desktop-only?",
initial: false,
},
{
type: "text",
name: "fundingUrl",
message: "Enter the funding URL",
},
{
type: "autocomplete",
name: "license",
message: `Choose a license ${reset(dim("(type to filter, ↑ or ↓ to navigate)"))}`,
initial: "MIT",
choices: Object.entries(licenses).map(([id, license]) => {
return {
value: id,
title: license.name,
description: (license.osiApproved && "OSI Approved") || "",
};
}),
},
],
{
onCancel: () => {
console.log(c.warning("❌ Generation cancelled"));
process.exit(0);
},
}
);
/**
* Readd recursive sync
* @param {string} dir
* @returns {fs.Dirent[]}
*/
function readdirRecursiveSync(dir) {
const results = [];
function readDirRecursive(currentPath) {
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
readDirRecursive(fullPath);
} else {
results.push(entry);
}
}
}
readDirRecursive(dir);
return results;
}
const templateFiles = readdirRecursiveSync("./src");
const data = {
name: answer.name || "Sample Plugin",
id: answer.id || "sample-plugin",
description: answer.description || "This is a sample plugin",
interfaceName: answer.name.replaceAll(" ", "") || "SamplePlugin",
author: {
url: answer.authorUrl || "",
name: answer.author || "Sample Author",
},
isDesktopOnly: !!answer.desktopOnly || false,
packageManager: getPackageManager(),
};
if (answer.fundingUrl) {
data.fundingUrl = answer.fundingUrl;
}
for (const file of templateFiles) {
ejsRender(file, data);
}
updateManifest(data, answer);
processReadme(data);
processCi(data);
processBugReport(data);
console.log(c.success("✅ Generated ") + c.info("all files"));
//update package.json
updatePackageJson(data, answer);
//create hotreload file
fs.writeFileSync(".hotreload", "", { encoding: "utf-8" });
//detect if yarn or npm or pnpm
await updateDep(data.packageManager);
//delete this files
fs.unlinkSync("generate.mjs");

30
hooks/_changelog.mjs Normal file
View file

@ -0,0 +1,30 @@
import { readFileSync, writeFile } from "fs";
import { Command } from "commander";
const program = new Command();
program.option("-b, --beta", "Pre-release version");
program.parse();
const opt = program.opts();
/**
* Remove text from the file
* @param {string} path
*/
function removeText(path) {
const toRemove = [
"# Changelog",
"All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.",
];
let changelog = readFileSync(path, "utf8");
for (const remove of toRemove) changelog = changelog.replace(remove, "").trim();
changelog = changelog.replaceAll(/[\n\r]{3,}/gm, "\n\n").trim();
changelog = changelog.replaceAll(/## (.*)[\n\r]{2}### /gm, "## $1\n### ").trim();
writeFile(path, changelog.trim(), "utf8", (err) => {
if (err) return console.error(err);
});
}
if (!opt.beta) removeText("CHANGELOG.md");
else removeText("CHANGELOG-beta.md");

14
manifest.json Normal file
View file

@ -0,0 +1,14 @@
{
"id": "<%= data.id %>",
"name": "<%= data.name %>",
"version": "0.0.0",
"minAppVersion": "1.5.12",
"description": "<%= data.description %>",
"author": "<%= data.author.name %>",
"authorUrl": "<%= data.author.url %>",
<%_ if (data.fundingURL) { _%>
"fundingUrl": "<%= data.fundingURL %>",
<%_
} _%>
"isDesktopOnly": <%= data.isDesktopOnly %>
}

54
package.json Normal file
View file

@ -0,0 +1,54 @@
{
"name": "sample-plugin",
"version": "0.0.0",
"description": "",
"main": "main.js",
"private": true,
"scripts": {
"prebuild": "tsc --noEmit --skipLibCheck",
"build": "node esbuild.config.mjs --production",
"dev:prod": "node esbuild.config.mjs --vault",
"dev": "node esbuild.config.mjs",
"export": "node esbuild.config.mjs --production --vault",
"bump": "dotenv -- node commit-and-tag-version.mjs",
"postbump": "dotenv -- if-env SKIP_POST_BUMP=true && echo skip bump || git push --follow-tags origin master",
"predeploy": "pnpm run bump",
"deploy": "pnpm run export",
"lint": "pnpm biome format --write src/"
},
"engines": {
"node": "^22.2.0"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.8.1",
"@types/node": "^16.11.6",
"builtin-modules": "4.0.0",
"i18next": "^23.16.4",
"esbuild": "0.21.5",
"obsidian": "latest",
"tslib": "2.6.3",
"typescript": "latest"
},
"dependencies": {
"@delucis/if-env": "^1.1.2",
"ansi-colors": "^4.1.3",
"commander": "^12.1.0",
"commit-and-tag-version": "^12.4.1",
"dedent": "^1.5.3",
"dotenv": "^16.4.5",
"dotenv-cli": "^7.4.2",
"ejs": "^3.1.10",
"execa": "^9.5.2",
"git-user-name": "^2.0.0",
"license": "^1.0.3",
"prompts": "^2.4.2",
"spdx-license-list": "^6.9.0"
},
"trustedDependencies": [
"@biomejs/biome",
"esbuild"
]
}

8
src/i18n/i18next.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
import type { resources } from "../i18n";
declare module "i18next" {
interface CustomTypeOptions {
readonly resources: (typeof resources)["en"];
readonly returnNull: false;
}
}

12
src/i18n/index.ts Normal file
View file

@ -0,0 +1,12 @@
import { moment } from "obsidian";
import en from "./locales/en.json";
//import fr from "./locales/fr.json";
export const resources = {
//fr: {translation: fr},
en: {translation: en}
};
export const translationLanguage = Object.keys(resources).find(i => i.toLocaleLowerCase() == moment.locale()) ? moment.locale() : "en";

0
src/i18n/locales/en.json Normal file
View file

9
src/interfaces.ts Normal file
View file

@ -0,0 +1,9 @@
export interface <%= data.interfaceName %>Settings {
mySetting: string;
}
export const DEFAULT_SETTINGS: <%= data.interfaceName %>Settings = {
mySetting: "default"
};

98
src/main.ts Normal file
View file

@ -0,0 +1,98 @@
import { type Editor, MarkdownView, Notice, Plugin, Modal } from "obsidian";
import { resources, translationLanguage } from "./i18n";
import i18next from "i18next";
import { <%= data.interfaceName %>SettingTab } from "./settings";
import { <%= data.interfaceName%>Modal } from "./modals";
import { <%= data.interfaceName%>Settings, DEFAULT_SETTINGS } from "./interfaces";
export default class <%= data.interfaceName %> extends Plugin {
settings!: <%= data.interfaceName %>Settings;
async onload() {
console.log(`[${this.manifest.name}] Loaded`)
await this.loadSettings();
//load i18next
await i18next.init({
lng: translationLanguage,
fallbackLng: "en",
resources,
returnNull: false,
returnEmptyString: false,
});
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon("dice", "Sample Plugin", (_evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice("This is a notice!");
});
// Perform additional things with the ribbon
ribbonIconEl.addClass("my-plugin-ribbon-class");
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText("Status Bar Text");
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: "open-sample-modal-simple",
name: "Open sample modal (simple)",
callback: () => {
new <%= data.interfaceName%>Modal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: "sample-editor-command",
name: "Sample editor command",
editorCallback: (editor: Editor, _view: MarkdownView|MarkdownFileInfo) => {
console.log(editor.getSelection());
editor.replaceSelection("Sample Editor Command");
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: "open-sample-modal-complex",
name: "Open sample modal (complex)",
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new <%= DataTransferItem={}.name%>Modal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new <%= data.interfaceName %>SettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, "click", (evt: MouseEvent) => {
console.log("click", evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log("setInterval"), 5 * 60 * 1000));
}
onunload() {
console.log(`[${this.manifest.name}] Unloaded`);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}

17
src/modals.ts Normal file
View file

@ -0,0 +1,17 @@
import { type App, Modal } from "obsidian";
export class <%= data.interfaceName%>Modal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText("Woah!");
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}

28
src/settings.ts Normal file
View file

@ -0,0 +1,28 @@
import { type App, PluginSettingTab, Setting } from "obsidian";
import <%= data.interfaceName %> from "./main";
export class <%= data.interfaceName %>SettingTab extends PluginSettingTab {
plugin: <%= data.interfaceName %>;
constructor(app: App, plugin: <%= data.interfaceName %>) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName("Setting #1")
.setDesc("It's a secret")
.addText(text => text
.setPlaceholder("Enter your secret")
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

8
src/styles.css Normal file
View file

@ -0,0 +1,8 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/

33
tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"noEmit": true,
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"module": "ESNext",
"target": "ESNext",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2015",
"ES2021",
"ES2022",
"DOM.Iterable"
]
},
"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 @@
{
"1.0.0": "0.15.0"
}