commit 05464c396a5aeb5544cbe0d4b5979bd57b154fae Author: Lisandra Date: Tue Dec 17 16:24:45 2024 +0100 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..84b8a66 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..e3e41a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..4700e37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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 diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml new file mode 100644 index 0000000..e6378d3 --- /dev/null +++ b/.github/workflows/auto_assign.yml @@ -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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4595d53 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d690ad --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4a2e560 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +tag-version-prefix="" +auto-install-peers=true +enable-pre-post-scripts=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..6eabf41 --- /dev/null +++ b/README.md @@ -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 from "./locales/.json";` + - Edit the `ressource` part with adding : ` : {translation: }` + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b9fca99 --- /dev/null +++ b/biome.json @@ -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" + } + } +} diff --git a/commit-and-tag-version.mjs b/commit-and-tag-version.mjs new file mode 100644 index 0000000..85f969e --- /dev/null +++ b/commit-and-tag-version.mjs @@ -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 ", "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); + }); +} diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..2ead8b9 --- /dev/null +++ b/esbuild.config.mjs @@ -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 ", "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(); +} diff --git a/generate.mjs b/generate.mjs new file mode 100644 index 0000000..a79d6cc --- /dev/null +++ b/generate.mjs @@ -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"); diff --git a/hooks/_changelog.mjs b/hooks/_changelog.mjs new file mode 100644 index 0000000..004e9be --- /dev/null +++ b/hooks/_changelog.mjs @@ -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"); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..798658b --- /dev/null +++ b/manifest.json @@ -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 %> +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c0eed03 --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/src/i18n/i18next.d.ts b/src/i18n/i18next.d.ts new file mode 100644 index 0000000..e6b4510 --- /dev/null +++ b/src/i18n/i18next.d.ts @@ -0,0 +1,8 @@ +import type { resources } from "../i18n"; + +declare module "i18next" { + interface CustomTypeOptions { + readonly resources: (typeof resources)["en"]; + readonly returnNull: false; + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..84f7f77 --- /dev/null +++ b/src/i18n/index.ts @@ -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"; \ No newline at end of file diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..e69de29 diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..abc7f55 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,9 @@ + + +export interface <%= data.interfaceName %>Settings { + mySetting: string; +} + +export const DEFAULT_SETTINGS: <%= data.interfaceName %>Settings = { + mySetting: "default" +}; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..403aaa1 --- /dev/null +++ b/src/main.ts @@ -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); + } +} + + diff --git a/src/modals.ts b/src/modals.ts new file mode 100644 index 0000000..8cfc342 --- /dev/null +++ b/src/modals.ts @@ -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(); + } +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..d589639 --- /dev/null +++ b/src/settings.ts @@ -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(); + })); + } +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..71cc60f --- /dev/null +++ b/src/styles.css @@ -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. + +*/ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d1426c --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..d409fa0 --- /dev/null +++ b/version-bump.mjs @@ -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")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..26382a1 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +}