diff --git a/.gitignore b/.gitignore index 622f443..01dae44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ dist/ node_modules/ publish/ +.env +tools/*.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea1030..26e338a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## not released + +## v1.0.1 (2021-08-12) + +- Fix: #13 `Hotfolder Path` is missing in settings and `Ignore Files` shows wrong value + ## v1.0.0 (2021-08-05) - Improved: Use registerSettings instead of deprecated registerSetting diff --git a/package-lock.json b/package-lock.json index 985588c..19aa6fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-hotfolder", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1379,6 +1379,15 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dev": true, + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", @@ -2467,6 +2476,12 @@ } } }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -3083,6 +3098,12 @@ "readable-stream": "^2.3.6" } }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5289,6 +5310,12 @@ } } }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, "mime-db": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", @@ -5428,6 +5455,12 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "dev": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index 2fcbb48..db9e0f3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "joplin-plugin-hotfolder", - "version": "1.0.0", + "version": "1.0.1", "scripts": { "dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", "prepare": "npm run test && npm run dist && husky install", "update": "npm install -g generator-joplin && yo joplin --update", + "compileTools": "tsc -p ./tools/tsconfig.json", + "release": "npm run compileTools && node ./tools/createRelease.js", + "gitRelease": "node ./tools/createRelease.js --upload", "test": "jest" }, "license": "MIT", @@ -14,13 +17,17 @@ "devDependencies": { "@types/jest": "^26.0.23", "@types/node": "^14.0.14", + "axios": "^0.21.1", "chalk": "^4.1.0", "copy-webpack-plugin": "^6.1.0", + "dotenv": "^10.0.0", "fs-extra": "^9.0.1", "glob": "^7.1.6", "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^11.0.0", + "mime": "^2.5.2", + "moment": "^2.29.1", "on-build-webpack": "^0.1.0", "prettier": "2.3.0", "tar": "^6.1.6", diff --git a/src/manifest.json b/src/manifest.json index 25c3835..b3c4150 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "id": "io.github.jackgruber.hotfolder", "app_min_version": "1.8.1", - "version": "1.0.0", + "version": "1.0.1", "name": "Hotfolder", "description": "Monitors a locale folder and import the files as a new note.", "author": "JackGruber", diff --git a/src/settings.ts b/src/settings.ts index 6731b4d..2e6b807 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -34,15 +34,14 @@ export namespace settings { label: "Hotfolder Path", }; - settingsObject["hotfolderPath" + (hotfolderNr == 0 ? "" : hotfolderNr)] = - { - value: ".*", - type: SettingItemType.String, - section: "hotfolderSection" + (hotfolderNr == 0 ? "" : hotfolderNr), - public: true, - label: "Ignore Files", - description: "Comma separated list of files which will be ignored.", - }; + settingsObject["ignoreFiles" + (hotfolderNr == 0 ? "" : hotfolderNr)] = { + value: ".*", + type: SettingItemType.String, + section: "hotfolderSection" + (hotfolderNr == 0 ? "" : hotfolderNr), + public: true, + label: "Ignore Files", + description: "Comma separated list of files which will be ignored.", + }; settingsObject[ "extensionsAddAsText" + (hotfolderNr == 0 ? "" : hotfolderNr) diff --git a/tools/createRelease.ts b/tools/createRelease.ts new file mode 100644 index 0000000..eb151be --- /dev/null +++ b/tools/createRelease.ts @@ -0,0 +1,134 @@ +import * as path from "path"; +import { getBranch, getInfo, nothingUncomitted } from "./git"; +import { + runNpmVersion, + setPluginVersion, + updateChangelog, + getJPLFileName, + getChangelog, +} from "./utils"; +import { + githubRelease, + ReleaseOptions, + AssetOptions, + githubAsset, + checkAuth, + ReproOptions, +} from "./github"; +import * as dotenv from "dotenv"; +import { execCommand } from "./execCommand"; + +async function createRelease() { + console.log("Create GitHub release"); + + const info = await getInfo(); + + const manifestFile = path.resolve( + path.join(__dirname, "../src/manifest.json") + ); + const manifest = require(manifestFile); + + const log = await getChangelog(manifest.version); + console.log(log); + + const releaseOptions: ReleaseOptions = { + owner: info.owner, + repo: info.repo, + tag: `v${manifest.version}`, + name: `v${manifest.version}`, + prerelease: false, + token: process.env.GITHUB_TOKEN, + body: log, + }; + + console.log("githubRelease"); + const releaseResult = await githubRelease(releaseOptions); + + const jpl = await getJPLFileName(); + + const releaseAssetOptions: AssetOptions = { + token: process.env.GITHUB_TOKEN, + asset: path.resolve(path.join(__dirname, "..", "publish", jpl)), + name: jpl, + label: jpl, + uploadUrl: releaseResult.upload_url, + }; + + console.log("githubAsset"); + await githubAsset(releaseAssetOptions); +} + +async function main() { + dotenv.config(); + + if ( + process.env.GITHUB_TOKEN === undefined || + process.env.GITHUB_TOKEN === "" + ) { + throw new Error("No GITHUB_TOKEN in env"); + } + const argv = require("yargs").argv; + + let type: string; + if (argv.upload) { + await createRelease(); + process.exit(0); + } else if (argv.patch) type = "patch"; + else if (argv.minor) type = "minor"; + else if (argv.major) type = "major"; + else throw new Error("--patch, --minor or --major not provided"); + + if (!(await nothingUncomitted())) { + throw new Error("Not a clean git status"); + } + + if ((await getBranch()) !== "develop") { + throw new Error("not in develop branch"); + } + + const info = await getInfo(); + console.log(info); + + const reproOptions: ReproOptions = { + owner: info.owner, + repo: info.repo, + token: process.env.GITHUB_TOKEN, + }; + if (!(await checkAuth(reproOptions))) { + throw new Error("Github auth error"); + } + console.log("Create release"); + await runNpmVersion(type); + const versionNumber = require(path.resolve( + path.join(__dirname, "../package.json") + )).version; + const version = `v${versionNumber}`; + console.log("new version " + version); + await setPluginVersion(versionNumber); + await updateChangelog(versionNumber); + + await execCommand( + "git add src/manifest.json CHANGELOG.md package-lock.json package.json" + ); + + await execCommand(`git commit -m "bump version ${versionNumber}"`); + await execCommand(`git checkout master`); + if ((await getBranch()) !== "master") { + throw new Error("not in master branch"); + } + + await execCommand(`git merge develop --no-ff`); + await execCommand(`git tag ${version}`); + + console.log("Execute the following commands:"); + console.log(`git push`); + console.log(`git push --tag`); + console.log(`npm publish`); + console.log(`npm run gitRelease`); +} + +main().catch((error) => { + console.error("Fatal error"); + console.error(error); + process.exit(1); +}); diff --git a/tools/execCommand.ts b/tools/execCommand.ts new file mode 100644 index 0000000..3df792f --- /dev/null +++ b/tools/execCommand.ts @@ -0,0 +1,137 @@ +// Source copied from Joplin +// https://github.com/laurent22/joplin/blob/5b1a9700448efb6aff423bda1889936c92393cbf/packages/tools/tool-utils.ts#L159-L164 + +import * as execa from "execa"; + +interface ExecCommandOptions { + showInput?: boolean; + showOutput?: boolean; + quiet?: boolean; +} + +export async function execCommand( + command: string | string[], + options: ExecCommandOptions = null +): Promise { + options = { + showInput: true, + showOutput: true, + quiet: false, + ...options, + }; + + if (options.quiet) { + options.showInput = false; + options.showOutput = false; + } + + if (options.showInput) { + if (typeof command === "string") { + console.info(`> ${command}`); + } else { + console.info(`> ${commandToString(command[0], command.slice(1))}`); + } + } + + const args: string[] = + typeof command === "string" + ? splitCommandString(command) + : (command as string[]); + const executableName = args[0]; + args.splice(0, 1); + const promise = execa(executableName, args); + if (options.showOutput) promise.stdout.pipe(process.stdout); + const result = await promise; + return result.stdout.trim(); +} + +function commandToString(commandName: string, args: string[] = []) { + const output = [quotePath(commandName)]; + + for (const arg of args) { + output.push(quotePath(arg)); + } + + return output.join(" "); +} + +function quotePath(path: string) { + if (!path) return ""; + if (path.indexOf('"') < 0 && path.indexOf(" ") < 0) return path; + path = path.replace(/"/, '\\"'); + return `"${path}"`; +} + +function splitCommandString(command, options = null) { + options = options || {}; + if (!("handleEscape" in options)) { + options.handleEscape = true; + } + + const args = []; + let state = "start"; + let current = ""; + let quote = '"'; + let escapeNext = false; + for (let i = 0; i < command.length; i++) { + const c = command[i]; + + if (state == "quotes") { + if (c != quote) { + current += c; + } else { + args.push(current); + current = ""; + state = "start"; + } + continue; + } + + if (escapeNext) { + current += c; + escapeNext = false; + continue; + } + + if (c == "\\" && options.handleEscape) { + escapeNext = true; + continue; + } + + if (c == '"' || c == "'") { + state = "quotes"; + quote = c; + continue; + } + + if (state == "arg") { + if (c == " " || c == "\t") { + args.push(current); + current = ""; + state = "start"; + } else { + current += c; + } + continue; + } + + if (c != " " && c != "\t") { + state = "arg"; + current += c; + } + } + + if (state == "quotes") { + throw new Error(`Unclosed quote in command line: ${command}`); + } + + if (current != "") { + args.push(current); + } + + if (args.length <= 0) { + throw new Error("Empty command line"); + } + + return args; +} diff --git a/tools/git.ts b/tools/git.ts new file mode 100644 index 0000000..5990804 --- /dev/null +++ b/tools/git.ts @@ -0,0 +1,45 @@ +import { execCommand } from "./execCommand"; + +interface GithubInfo { + owner: string; + repo: string; +} + +export async function nothingUncomitted() { + console.log("Check git status"); + const status = await execCommand("git status --porcelain", { + showOutput: true, + showInput: false, + }); + + if (status.trim() === "") return true; + else return false; +} + +export async function getBranch() { + return await execCommand("git rev-parse --abbrev-ref HEAD", { + showOutput: false, + showInput: false, + }); +} +export async function getInfo(): Promise { + const dataStr = await execCommand("git remote -v", { + showOutput: false, + showInput: false, + }); + + for (const str of dataStr.split("\n")) { + if ( + str.slice(0, 6) === "origin" && + str.slice(str.length - 5, str.length - 1) === "push" + ) { + const tmp = str.split(/[\:/.]/); + + const owner = tmp[tmp.length - 3]; + const repo = tmp[tmp.length - 2]; + return { owner: owner, repo: repo }; + } + } + + return null; +} diff --git a/tools/github.ts b/tools/github.ts new file mode 100644 index 0000000..064298b --- /dev/null +++ b/tools/github.ts @@ -0,0 +1,99 @@ +// https://docs.github.com/en/rest/reference/repos + +import axios from "axios"; +import * as FormData from "form-data"; +import * as fs from "fs-extra"; +import * as mime from "mime"; + +const apiRoot = "https://api.github.com"; + +export interface AssetOptions { + asset: string; + token: string; + uploadUrl: string; + label: string; + name: string; +} + +export interface ReleaseOptions { + name: string; + owner: string; + repo: string; + tag: string; + body: string; + prerelease: boolean; + token: string; +} + +export interface ReproOptions { + owner: string; + repo: string; + token?: string; +} + +export async function checkAuth(options: ReproOptions): Promise { + const url = `${apiRoot}/repos/${options.owner}/${options.repo}/releases`; + const headers = { + Authorization: `token ${options.token}`, + accept: `application/vnd.github.v3+json`, + }; + + const response = await axios.get(url, { headers }); + + if (response.status === 200 && response.statusText === "OK") { + return true; + } else { + return false; + } +} + +export async function githubRelease(options: ReleaseOptions): Promise { + const url = `${apiRoot}/repos/${options.owner}/${options.repo}/releases`; + const body = { + tag_name: options.tag, + name: options.name, + body: options.body, + prerelease: options.prerelease, + }; + const headers = { + Authorization: `token ${options.token}`, + accept: `application/vnd.github.v3+json`, + }; + const response = await axios.post(url, body, { headers }); + if (response.status !== 201) { + console.error(response); + throw new Error("github release error"); + } + return response.data; +} + +export async function githubAsset(info: AssetOptions): Promise { + const cleanUrl = info.uploadUrl.replace("{?name,label}", ""); + const form = new FormData(); + form.append("file", fs.createReadStream(info.asset)); + const state = fs.statSync(info.asset); + const headers = { + Authorization: `token ${info.token}`, + "Content-Type": mime.getType(info.asset), + "Content-Length": state.size, + accept: `application/vnd.github.v3+json`, + }; + + const response = await axios.post( + `${cleanUrl}?label=${info.label}&name=${info.name}`, + form, + { headers } + ); + + if (response.status !== 201) { + console.error(response); + throw new Error("github asset upload error"); + } + + if (response.data.state !== "uploaded") { + console.error(response); + throw new Error("github asset upload error"); + } + + return response.data; +} diff --git a/tools/tsconfig.json b/tools/tsconfig.json new file mode 100644 index 0000000..69a88e3 --- /dev/null +++ b/tools/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/tools/utils.ts b/tools/utils.ts new file mode 100644 index 0000000..ce302bf --- /dev/null +++ b/tools/utils.ts @@ -0,0 +1,67 @@ +import * as fs from "fs-extra"; +import * as moment from "moment"; +import * as path from "path"; +import { execCommand } from "./execCommand"; + +export function readline(question: string) { + return new Promise((resolve) => { + const readline = require("readline"); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question(`${question} `, (answer: string) => { + resolve(answer); + rl.close(); + }); + }); +} + +export async function getJPLFileName() { + const manifestFile = path.resolve( + path.join(__dirname, "..", "src", "manifest.json") + ); + const manifest = require(manifestFile); + return manifest.id + ".jpl"; +} + +export async function updateChangelog(version: string) { + const changelog = path.resolve(path.join(__dirname, "..", "CHANGELOG.md")); + + let data = fs.readFileSync(changelog, { encoding: "utf8", flag: "r" }); + version = `${version} (${moment().format("YYYY-MM-DD")})`; + data = data.replace(/## not released/, `## not released\n\n## v${version}`); + fs.writeFileSync(changelog, data); +} + +export async function setPluginVersion(version: string) { + console.log("set Joplin plugin version"); + const manifestFile = path.resolve( + path.join(__dirname, "../src/manifest.json") + ); + const manifest = require(manifestFile); + manifest.version = version; + fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2)); +} + +export async function runNpmVersion(type: string) { + console.log("bump npm version"); + await execCommand( + `npm version ${type} -git-tag-version false -commit-hooks false`, + { showOutput: true, showInput: true } + ); +} + +export async function getChangelog(version: string): Promise { + const changelog = path.resolve(path.join(__dirname, "..", "CHANGELOG.md")); + let data = fs.readFileSync(changelog, { encoding: "utf8", flag: "r" }); + + version = version.replace(/\./g, "\\."); + const regExp = new RegExp(`(?## v${version}(.|\n)*?)^##`, "im"); + const match = data.match(regExp); + let change = match.groups.ENTRY.split("\n"); + change.splice(0, 2); + return change.join("\n"); +}