diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json index 7d53cdbe98..4d6e95e200 100644 --- a/.npmpackagejsonlintrc.json +++ b/.npmpackagejsonlintrc.json @@ -14,7 +14,8 @@ "@joplin/turndown-plugin-gfm", "@joplin/tools", "@joplin/react-native-saf-x", - "@joplin/react-native-alarm-notification" + "@joplin/react-native-alarm-notification", + "@joplin/utils" ] } ] diff --git a/joplin.code-workspace b/joplin.code-workspace index d784f5dc24..7ebd719560 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -329,6 +329,7 @@ "packages/renderer/MdToHtml/rules/sanitize_html.js": true, "packages/server/db-*.sqlite": true, "packages/server/dist/": true, + "packages/utils/dist/": true, "packages/server/temp": true, "packages/server/test.pid": true, "phpunit.xml": true, diff --git a/packages/app-cli/app/app-gui.js b/packages/app-cli/app/app-gui.js index 3f468aebe0..cd296d3128 100644 --- a/packages/app-cli/app/app-gui.js +++ b/packages/app-cli/app/app-gui.js @@ -8,7 +8,7 @@ const Resource = require('@joplin/lib/models/Resource').default; const Setting = require('@joplin/lib/models/Setting').default; const reducer = require('@joplin/lib/reducer').default; const { defaultState } = require('@joplin/lib/reducer'); -const { splitCommandString } = require('@joplin/lib/string-utils.js'); +const { splitCommandString } = require('@joplin/utils'); const { reg } = require('@joplin/lib/registry.js'); const { _ } = require('@joplin/lib/locale'); const shim = require('@joplin/lib/shim').default; diff --git a/packages/app-cli/app/app.js b/packages/app-cli/app/app.js index 37392f266b..85a45fdcf3 100644 --- a/packages/app-cli/app/app.js +++ b/packages/app-cli/app/app.js @@ -9,7 +9,8 @@ const Tag = require('@joplin/lib/models/Tag').default; const Setting = require('@joplin/lib/models/Setting').default; const { reg } = require('@joplin/lib/registry.js'); const { fileExtension } = require('@joplin/lib/path-utils'); -const { splitCommandString, splitCommandBatch } = require('@joplin/lib/string-utils'); +const { splitCommandString } = require('@joplin/utils'); +const { splitCommandBatch } = require('@joplin/lib/string-utils'); const { _ } = require('@joplin/lib/locale'); const fs = require('fs-extra'); const { cliUtils } = require('./cli-utils.js'); diff --git a/packages/app-cli/app/command-edit.js b/packages/app-cli/app/command-edit.js index 4eca9b9e1f..013ca7344d 100644 --- a/packages/app-cli/app/command-edit.js +++ b/packages/app-cli/app/command-edit.js @@ -1,6 +1,6 @@ const fs = require('fs-extra'); const BaseCommand = require('./base-command').default; -const { splitCommandString } = require('@joplin/lib/string-utils.js'); +const { splitCommandString } = require('@joplin/utils'); const uuid = require('@joplin/lib/uuid').default; const { app } = require('./app.js'); const { _ } = require('@joplin/lib/locale'); diff --git a/packages/app-cli/package.json b/packages/app-cli/package.json index bf07e992df..17cf7607b1 100644 --- a/packages/app-cli/package.json +++ b/packages/app-cli/package.json @@ -30,7 +30,8 @@ 2019, 2020, 2021, - 2022 + 2022, + 2023 ], "owner": "Laurent Cozic" }, @@ -42,6 +43,7 @@ "dependencies": { "@joplin/lib": "~2.10", "@joplin/renderer": "~2.10", + "@joplin/utils": "~2.10", "aws-sdk": "2.1290.0", "chalk": "4.1.2", "compare-version": "0.1.2", diff --git a/packages/app-cli/tools/populateDatabase.ts b/packages/app-cli/tools/populateDatabase.ts index 04ffb340be..5b2a81268e 100644 --- a/packages/app-cli/tools/populateDatabase.ts +++ b/packages/app-cli/tools/populateDatabase.ts @@ -19,7 +19,7 @@ import * as fs from 'fs-extra'; import { homedir } from 'os'; -import { execCommand2 } from '@joplin/tools/tool-utils'; +import { execCommand } from '@joplin/utils'; import { chdir } from 'process'; const minUserNum = 1; @@ -66,7 +66,7 @@ const processUser = async (userNum: number) => { await chdir(cliDir); - await execCommand2(['yarn', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]); + await execCommand(['yarn', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]); } catch (error) { console.error(`Could not process user ${userNum}:`, error); } finally { @@ -90,7 +90,7 @@ const main = async () => { // Build the app once before starting, because we'll use start-no-build to // run the scripts (faster) - await execCommand2(['yarn', 'run', 'build']); + await execCommand(['yarn', 'run', 'build']); const focusUserNum = 0; diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index f451957481..fba7a57d82 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -20,7 +20,7 @@ import Folder from './models/Folder'; import BaseItem from './models/BaseItem'; import Note from './models/Note'; import Tag from './models/Tag'; -const { splitCommandString } = require('./string-utils.js'); +import { splitCommandString } from '@joplin/utils'; import { reg } from './registry'; import time from './time'; import BaseSyncTarget from './BaseSyncTarget'; @@ -706,7 +706,7 @@ export default class BaseApplication { flagContent = flagContent.trim(); - let flags = splitCommandString(flagContent); + let flags: any = splitCommandString(flagContent); flags.splice(0, 0, 'cmd'); flags.splice(0, 0, 'node'); diff --git a/packages/lib/package.json b/packages/lib/package.json index 50653fedd7..867848d879 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -38,6 +38,7 @@ "@joplin/renderer": "^2.10.2", "@joplin/turndown": "^4.0.65", "@joplin/turndown-plugin-gfm": "^1.0.47", + "@joplin/utils": "~2.10", "@types/nanoid": "3.0.0", "async-mutex": "0.4.0", "base-64": "1.0.0", diff --git a/packages/lib/services/ExternalEditWatcher/utils.ts b/packages/lib/services/ExternalEditWatcher/utils.ts index 52390350ed..ade384e5a5 100644 --- a/packages/lib/services/ExternalEditWatcher/utils.ts +++ b/packages/lib/services/ExternalEditWatcher/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable import/prefer-default-export */ -const { splitCommandString } = require('../../string-utils'); +import { splitCommandString } from '@joplin/utils'; import { spawn } from 'child_process'; import Logger from '../../Logger'; import Setting from '../../models/Setting'; diff --git a/packages/lib/string-utils.js b/packages/lib/string-utils.js index 3952228d84..b1530e9357 100644 --- a/packages/lib/string-utils.js +++ b/packages/lib/string-utils.js @@ -138,80 +138,6 @@ function commandArgumentsToString(args) { return output.join(' '); } -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; -} - function splitCommandBatch(commandBatch) { const commandLines = []; const eol = '\n'; @@ -368,4 +294,4 @@ function scriptType(s) { return 'en'; } -module.exports = Object.assign({ formatCssSize, camelCaseToDash, removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandString, splitCommandBatch, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon); +module.exports = Object.assign({ formatCssSize, camelCaseToDash, removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandBatch, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon); diff --git a/packages/plugin-repo-cli/index.ts b/packages/plugin-repo-cli/index.ts index 9136f9c109..ab09a6fd69 100644 --- a/packages/plugin-repo-cli/index.ts +++ b/packages/plugin-repo-cli/index.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import * as process from 'process'; import validatePluginId from '@joplin/lib/services/plugins/utils/validatePluginId'; import validatePluginVersion from '@joplin/lib/services/plugins/utils/validatePluginVersion'; -import { execCommand2, resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js'; +import { resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js'; import checkIfPluginCanBeAdded from './lib/checkIfPluginCanBeAdded'; import updateReadme from './lib/updateReadme'; import { NpmPackage } from './lib/types'; @@ -17,6 +17,7 @@ import gitCompareUrl from './lib/gitCompareUrl'; import commandUpdateRelease from './commands/updateRelease'; import { isJoplinPluginPackage, readJsonFile } from './lib/utils'; import { applyManifestOverrides, getObsoleteManifests, readManifestOverrides } from './lib/overrideUtils'; +import { execCommand } from '@joplin/utils'; function pluginInfoFromSearchResults(results: any[]): NpmPackage[] { const output: NpmPackage[] = []; @@ -49,7 +50,7 @@ async function checkPluginRepository(dirPath: string, dryRun: boolean) { async function extractPluginFilesFromPackage(existingManifests: any, workDir: string, packageName: string, destDir: string): Promise { const previousDir = chdir(workDir); - await execCommand2(`npm install ${packageName} --save --ignore-scripts`, { showStderr: false, showStdout: false }); + await execCommand(`npm install ${packageName} --save --ignore-scripts`, { showStderr: false, showStdout: false }); const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish'); @@ -154,7 +155,7 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string, dryRun await fs.mkdirp(packageTempDir); chdir(packageTempDir); - await execCommand2('npm init --yes --loglevel silent', { quiet: true }); + await execCommand('npm init --yes --loglevel silent', { quiet: true }); let actionType: ProcessingActionType = ProcessingActionType.Update; let manifests: any = {}; @@ -200,8 +201,8 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string, dryRun if (!dryRun) { if (!(await gitRepoClean())) { - await execCommand2('git add -A', { showStdout: false }); - await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showStdout: false }); + await execCommand('git add -A', { showStdout: false }); + await execCommand(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showStdout: false }); } else { console.info('Nothing to commit'); } @@ -227,14 +228,14 @@ async function commandBuild(args: CommandBuildArgs) { if (!dryRun) { if (!(await gitRepoClean())) { console.info('Updating README...'); - await execCommand2('git add -A'); - await execCommand2('git commit -m "Update README"'); + await execCommand('git add -A'); + await execCommand('git commit -m "Update README"'); } } chdir(previousDir); - const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showStdout: false, showStderr: false })).trim(); + const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json', { showStdout: false, showStderr: false })).trim(); const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults)); for (const npmPackage of npmPackages) { @@ -245,11 +246,11 @@ async function commandBuild(args: CommandBuildArgs) { await commandUpdateRelease(args); if (!(await gitRepoClean())) { - await execCommand2('git add -A'); - await execCommand2('git commit -m "Update stats"'); + await execCommand('git add -A'); + await execCommand('git commit -m "Update stats"'); } - await execCommand2('git push'); + await execCommand('git push'); } } diff --git a/packages/plugin-repo-cli/package.json b/packages/plugin-repo-cli/package.json index 02680cde51..04e87491f6 100644 --- a/packages/plugin-repo-cli/package.json +++ b/packages/plugin-repo-cli/package.json @@ -20,6 +20,7 @@ "dependencies": { "@joplin/lib": "^2.10.2", "@joplin/tools": "^2.10.2", + "@joplin/utils": "~2.10", "fs-extra": "11.1.0", "gh-release-assets": "2.0.1", "node-fetch": "2.6.7", diff --git a/packages/tools/buildServerDocker.ts b/packages/tools/buildServerDocker.ts index e88cb6b045..479bb49b26 100644 --- a/packages/tools/buildServerDocker.ts +++ b/packages/tools/buildServerDocker.ts @@ -1,5 +1,6 @@ -import { execCommand2, rootDir } from './tool-utils'; +import { rootDir } from './tool-utils'; import * as moment from 'moment'; +import { execCommand } from '@joplin/utils'; interface Argv { dryRun?: boolean; @@ -35,7 +36,7 @@ async function main() { const buildDate = moment(new Date().getTime()).format('YYYY-MM-DDTHH:mm:ssZ'); let revision = ''; try { - revision = await execCommand2('git rev-parse --short HEAD', { showStdout: false }); + revision = await execCommand('git rev-parse --short HEAD', { showStdout: false }); } catch (error) { console.info('Could not get git commit: metadata revision field will be empty'); } @@ -62,11 +63,11 @@ async function main() { return; } - await execCommand2(dockerCommand); + await execCommand(dockerCommand); for (const tag of dockerTags) { - await execCommand2(`docker tag "${repository}:${imageVersion}" "${repository}:${tag}"`); - if (pushImages) await execCommand2(`docker push ${repository}:${tag}`); + await execCommand(`docker tag "${repository}:${imageVersion}" "${repository}:${tag}"`); + if (pushImages) await execCommand(`docker push ${repository}:${tag}`); } } diff --git a/packages/tools/bundleDefaultPlugins.ts b/packages/tools/bundleDefaultPlugins.ts index 0000e6002f..030ea727ba 100644 --- a/packages/tools/bundleDefaultPlugins.ts +++ b/packages/tools/bundleDefaultPlugins.ts @@ -1,8 +1,8 @@ import { join } from 'path'; -import { execCommand2 } from './tool-utils'; import { pathExists, mkdir, readFile, move, remove, writeFile } from 'fs-extra'; import { DefaultPluginsInfo } from '@joplin/lib/services/plugins/PluginService'; import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo'; +import { execCommand } from '@joplin/utils'; const fetch = require('node-fetch'); interface PluginAndVersion { @@ -41,7 +41,7 @@ async function downloadFile(url: string, outputPath: string) { export async function extractPlugins(currentDir: string, defaultPluginDir: string, downloadedPluginsNames: PluginIdAndName): Promise { for (const pluginId of Object.keys(downloadedPluginsNames)) { - await execCommand2(`tar xzf ${currentDir}/${downloadedPluginsNames[pluginId]}`, { quiet: true }); + await execCommand(`tar xzf ${currentDir}/${downloadedPluginsNames[pluginId]}`, { quiet: true }); await move(`package/publish/${pluginId}.jpl`, `${defaultPluginDir}/${pluginId}/plugin.jpl`, { overwrite: true }); await move(`package/publish/${pluginId}.json`, `${defaultPluginDir}/${pluginId}/manifest.json`, { overwrite: true }); await remove(`${downloadedPluginsNames[pluginId]}`); diff --git a/packages/tools/generate-database-types.ts b/packages/tools/generate-database-types.ts index fe7def500e..ef3afdc809 100644 --- a/packages/tools/generate-database-types.ts +++ b/packages/tools/generate-database-types.ts @@ -1,4 +1,5 @@ -import { execCommand2, rootDir } from './tool-utils'; +import { execCommand } from '@joplin/utils'; +import { rootDir } from './tool-utils'; const sqlts = require('@rmp135/sql-ts').default; const fs = require('fs-extra'); @@ -6,7 +7,7 @@ const fs = require('fs-extra'); async function main() { // Run the CLI app once so as to generate the database file process.chdir(`${rootDir}/packages/app-cli`); - await execCommand2('yarn start version'); + await execCommand('yarn start version'); const sqlTsConfig = { 'client': 'sqlite3', diff --git a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js index 94c3e13ffc..91d6b3e7b1 100644 --- a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js +++ b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js @@ -36,6 +36,7 @@ module.exports = { 'packages/fork-sax/**', 'packages/lib/plugin_types/**', 'packages/server/**', + 'packages/utils/**', ], }).filter(f => !f.endsWith('.d.ts')); diff --git a/packages/tools/licenseChecker.ts b/packages/tools/licenseChecker.ts index 816fa185cf..7f4bb16b51 100644 --- a/packages/tools/licenseChecker.ts +++ b/packages/tools/licenseChecker.ts @@ -1,6 +1,7 @@ import { readdir, stat, writeFile } from 'fs-extra'; import { chdir, cwd } from 'process'; -import { execCommand2, rootDir } from './tool-utils'; +import { rootDir } from './tool-utils'; +import { execCommand } from '@joplin/utils'; import yargs = require('yargs'); import { rtrimSlashes } from '@joplin/lib/path-utils'; @@ -13,7 +14,7 @@ interface LicenseInfo { const getLicenses = async (directory: string): Promise> => { const previousDir = cwd(); await chdir(directory); - const result = await execCommand2(['license-checker-rseidelsohn', '--production', '--json'], { quiet: true }); + const result = await execCommand(['license-checker-rseidelsohn', '--production', '--json'], { quiet: true }); const info: Record = JSON.parse(result); if (!info) throw new Error(`Could not parse JSON: ${directory}`); await chdir(previousDir); diff --git a/packages/tools/package.json b/packages/tools/package.json index d4e4e7cf84..c3cdd683da 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -22,6 +22,7 @@ "dependencies": { "@joplin/lib": "^2.10.2", "@joplin/renderer": "^2.10.2", + "@joplin/utils": "~2.10", "@types/node-fetch": "2.6.2", "@types/yargs": "17.0.20", "dayjs": "1.11.7", diff --git a/packages/tools/release-android.ts b/packages/tools/release-android.ts index 4eb33911b4..c44bdcd085 100644 --- a/packages/tools/release-android.ts +++ b/packages/tools/release-android.ts @@ -1,5 +1,6 @@ +import { execCommand } from '@joplin/utils'; import * as fs from 'fs-extra'; -import { execCommandVerbose, execCommandWithPipes, githubRelease, githubOauthToken, fileExists, gitPullTry, completeReleaseWithChangelog, execCommand2 } from './tool-utils'; +import { execCommandVerbose, execCommandWithPipes, githubRelease, githubOauthToken, fileExists, gitPullTry, completeReleaseWithChangelog } from './tool-utils'; const path = require('path'); const fetch = require('node-fetch'); const uriTemplate = require('uri-template'); @@ -150,7 +151,7 @@ async function main() { const isPreRelease = !('type' in argv) || argv.type === 'prerelease'; process.chdir(rnDir); - await execCommand2('yarn run build', { showStdout: false }); + await execCommand('yarn run build', { showStdout: false }); if (isPreRelease) console.info('Creating pre-release'); console.info('Updating version numbers in build.gradle...'); diff --git a/packages/tools/release-cli.ts b/packages/tools/release-cli.ts index 35f7314255..7a2dd18ea9 100644 --- a/packages/tools/release-cli.ts +++ b/packages/tools/release-cli.ts @@ -1,4 +1,5 @@ -import { execCommand2, rootDir, completeReleaseWithChangelog } from './tool-utils'; +import { execCommand } from '@joplin/utils'; +import { rootDir, completeReleaseWithChangelog } from './tool-utils'; const appDir = `${rootDir}/packages/app-cli`; const changelogPath = `${rootDir}/readme/changelog_cli.md`; @@ -8,19 +9,19 @@ const changelogPath = `${rootDir}/readme/changelog_cli.md`; async function main() { process.chdir(appDir); - await execCommand2('git pull'); + await execCommand('git pull'); - const newVersion = (await execCommand2('npm version patch')).trim(); + const newVersion = (await execCommand('npm version patch')).trim(); console.info(`Building ${newVersion}...`); const newTag = `cli-${newVersion}`; - await execCommand2('touch app/main.js'); - await execCommand2('yarn run build'); - await execCommand2('cp ../../README.md build/'); + await execCommand('touch app/main.js'); + await execCommand('yarn run build'); + await execCommand('cp ../../README.md build/'); process.chdir(`${appDir}/build`); - await execCommand2('npm publish'); + await execCommand('npm publish'); await completeReleaseWithChangelog(changelogPath, newVersion, newTag, 'CLI', false); } diff --git a/packages/tools/release-electron.ts b/packages/tools/release-electron.ts index 4cb1bdd94e..0245114361 100644 --- a/packages/tools/release-electron.ts +++ b/packages/tools/release-electron.ts @@ -1,4 +1,5 @@ -import { execCommand2, gitCurrentBranch, githubRelease, gitPullTry, rootDir } from './tool-utils'; +import { execCommand } from '@joplin/utils'; +import { gitCurrentBranch, githubRelease, gitPullTry, rootDir } from './tool-utils'; const appDir = `${rootDir}/packages/app-desktop`; @@ -11,16 +12,16 @@ async function main() { console.info(`Running from: ${process.cwd()}`); - const version = (await execCommand2('npm version patch')).trim(); + const version = (await execCommand('npm version patch')).trim(); const tagName = version; console.info(`New version number: ${version}`); - await execCommand2('git add -A'); - await execCommand2(`git commit -m "Desktop release ${version}"`); - await execCommand2(`git tag ${tagName}`); - await execCommand2('git push'); - await execCommand2('git push --tags'); + await execCommand('git add -A'); + await execCommand(`git commit -m "Desktop release ${version}"`); + await execCommand(`git tag ${tagName}`); + await execCommand('git push'); + await execCommand('git push --tags'); const releaseOptions = { isDraft: true, isPreRelease: !!argv.beta }; diff --git a/packages/tools/release-plugin-repo-cli.ts b/packages/tools/release-plugin-repo-cli.ts index b99176b2f8..6aa6160b58 100644 --- a/packages/tools/release-plugin-repo-cli.ts +++ b/packages/tools/release-plugin-repo-cli.ts @@ -1,5 +1,6 @@ +import { execCommand } from '@joplin/utils'; import { chdir } from 'process'; -import { rootDir, gitPullTry, execCommand2, releaseFinalGitCommands } from './tool-utils'; +import { rootDir, gitPullTry, releaseFinalGitCommands } from './tool-utils'; const workDir = `${rootDir}/packages/plugin-repo-cli`; @@ -7,18 +8,18 @@ async function main() { await gitPullTry(); chdir(rootDir); - await execCommand2('yarn run tsc'); + await execCommand('yarn run tsc'); chdir(workDir); - await execCommand2('yarn run dist'); + await execCommand('yarn run dist'); - const newVersion = (await execCommand2('npm version patch')).trim(); + const newVersion = (await execCommand('npm version patch')).trim(); console.info(`New version: ${newVersion}`); const tagName = `plugin-repo-cli-${newVersion}`; console.info(`Tag name: ${tagName}`); - await execCommand2('npm publish'); + await execCommand('npm publish'); console.info(releaseFinalGitCommands('Plugin Repo CLI', newVersion, tagName)); } diff --git a/packages/tools/release-server.ts b/packages/tools/release-server.ts index 37e7dfd6b0..ef63897a61 100644 --- a/packages/tools/release-server.ts +++ b/packages/tools/release-server.ts @@ -1,4 +1,5 @@ -import { execCommand2, rootDir, gitPullTry, completeReleaseWithChangelog } from './tool-utils'; +import { execCommand } from '@joplin/utils'; +import { rootDir, gitPullTry, completeReleaseWithChangelog } from './tool-utils'; const serverDir = `${rootDir}/packages/server`; @@ -12,7 +13,7 @@ async function main() { await gitPullTry(); process.chdir(serverDir); - const version = (await execCommand2('npm version patch')).trim(); + const version = (await execCommand('npm version patch')).trim(); const versionSuffix = ''; // isPreRelease ? '-beta' : ''; const tagName = `server-${version}${versionSuffix}`; diff --git a/packages/tools/spellcheck.ts b/packages/tools/spellcheck.ts index 748975ef62..19bc3d722b 100644 --- a/packages/tools/spellcheck.ts +++ b/packages/tools/spellcheck.ts @@ -1,6 +1,7 @@ import yargs = require('yargs'); import { chdir } from 'process'; -import { execCommand2, rootDir } from './tool-utils'; +import { rootDir } from './tool-utils'; +import { execCommand } from '@joplin/utils'; const main = async () => { const argv = await yargs.argv; @@ -10,7 +11,7 @@ const main = async () => { chdir(rootDir); try { - await execCommand2(['yarn', 'run', 'cspell'].concat(filePaths), { showStderr: false, showStdout: false }); + await execCommand(['yarn', 'run', 'cspell'].concat(filePaths), { showStderr: false, showStdout: false }); } catch (error) { if (!error.stdout.trim()) return; diff --git a/packages/tools/tagServerLatest.ts b/packages/tools/tagServerLatest.ts index e744ef1553..8bb558308c 100644 --- a/packages/tools/tagServerLatest.ts +++ b/packages/tools/tagServerLatest.ts @@ -1,4 +1,4 @@ -import { execCommand2 } from './tool-utils'; +import { execCommand } from '@joplin/utils'; async function main() { const argv = require('yargs').argv; @@ -6,9 +6,9 @@ async function main() { const version = argv._[0]; - await execCommand2(`docker pull "joplin/server:${version}"`); - await execCommand2(`docker tag "joplin/server:${version}" "joplin/server:latest"`); - await execCommand2('docker push joplin/server:latest'); + await execCommand(`docker pull "joplin/server:${version}"`); + await execCommand(`docker tag "joplin/server:${version}" "joplin/server:latest"`); + await execCommand('docker push joplin/server:latest'); } if (require.main === module) { diff --git a/packages/tools/tool-utils.ts b/packages/tools/tool-utils.ts index e89604ef40..dca4821482 100644 --- a/packages/tools/tool-utils.ts +++ b/packages/tools/tool-utils.ts @@ -1,9 +1,9 @@ import * as fs from 'fs-extra'; import { readCredentialFile } from '@joplin/lib/utils/credentialFiles'; +import { execCommand as execCommand2, commandToString } from '@joplin/utils'; const fetch = require('node-fetch'); const execa = require('execa'); -const { splitCommandString } = require('@joplin/lib/string-utils'); const moment = require('moment'); export interface GitHubReleaseAsset { @@ -20,23 +20,6 @@ export interface GitHubRelease { draft: boolean; } -function quotePath(path: string) { - if (!path) return ''; - if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path; - path = path.replace(/"/, '\\"'); - return `"${path}"`; -} - -function commandToString(commandName: string, args: string[] = []) { - const output = [quotePath(commandName)]; - - for (const arg of args) { - output.push(quotePath(arg)); - } - - return output.join(' '); -} - async function insertChangelog(tag: string, changelogPath: string, changelog: string, isPrerelease: boolean, repoTagUrl: string = '') { repoTagUrl = repoTagUrl || 'https://github.com/laurent22/joplin/releases/tag'; @@ -163,52 +146,6 @@ export function execCommandVerbose(commandName: string, args: string[] = []) { return promise; } -interface ExecCommandOptions { - showInput?: boolean; - showStdout?: boolean; - showStderr?: boolean; - quiet?: boolean; -} - -// There's lot of execCommandXXX functions, but eventually all scripts should -// use the one below, which supports: -// -// - Printing the command being executed -// - Printing the output in real time (piping to stdout) -// - Returning the command result as string -export async function execCommand2(command: string | string[], options: ExecCommandOptions = null): Promise { - options = { - showInput: true, - showStdout: true, - showStderr: true, - quiet: false, - ...options, - }; - - if (options.quiet) { - options.showInput = false; - options.showStdout = false; - options.showStderr = 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.showStdout) promise.stdout.pipe(process.stdout); - if (options.showStderr) promise.stdout.pipe(process.stderr); - const result = await promise; - return result.stdout.trim(); -} - export function execCommandWithPipes(executable: string, args: string[]) { const spawn = require('child_process').spawn; diff --git a/packages/tools/updateMarkdownDoc.ts b/packages/tools/updateMarkdownDoc.ts index ce7efe2493..f71dd9eb43 100644 --- a/packages/tools/updateMarkdownDoc.ts +++ b/packages/tools/updateMarkdownDoc.ts @@ -1,5 +1,6 @@ +import { execCommand } from '@joplin/utils'; import { chdir } from 'process'; -import { execCommand2, rootDir, gitRepoCleanTry } from './tool-utils'; +import { rootDir, gitRepoCleanTry } from './tool-utils'; import updateDownloadPage from './website/updateDownloadPage'; async function main() { @@ -7,23 +8,23 @@ async function main() { if (doGitOperations) { await gitRepoCleanTry(); - await execCommand2(['git', 'pull', '--rebase']); + await execCommand(['git', 'pull', '--rebase']); } - await execCommand2(['node', `${rootDir}/packages/tools/update-readme-download.js`]); - await execCommand2(['node', `${rootDir}/packages/tools/build-release-stats.js`, '--types=changelog']); - await execCommand2(['node', `${rootDir}/packages/tools/build-release-stats.js`, '--types=stats', '--update-interval=30']); - await execCommand2(['node', `${rootDir}/packages/tools/update-readme-sponsors.js`]); - await execCommand2(['node', `${rootDir}/packages/tools/build-welcome.js`]); + await execCommand(['node', `${rootDir}/packages/tools/update-readme-download.js`]); + await execCommand(['node', `${rootDir}/packages/tools/build-release-stats.js`, '--types=changelog']); + await execCommand(['node', `${rootDir}/packages/tools/build-release-stats.js`, '--types=stats', '--update-interval=30']); + await execCommand(['node', `${rootDir}/packages/tools/update-readme-sponsors.js`]); + await execCommand(['node', `${rootDir}/packages/tools/build-welcome.js`]); chdir(rootDir); - await execCommand2(['yarn', 'run', 'buildApiDoc']); + await execCommand(['yarn', 'run', 'buildApiDoc']); await updateDownloadPage(); if (doGitOperations) { - await execCommand2(['git', 'add', '-A']); - await execCommand2(['git', 'commit', '-m', 'Update Markdown doc']); - await execCommand2(['git', 'pull', '--rebase']); - await execCommand2(['git', 'push']); + await execCommand(['git', 'add', '-A']); + await execCommand(['git', 'commit', '-m', 'Update Markdown doc']); + await execCommand(['git', 'pull', '--rebase']); + await execCommand(['git', 'push']); } } diff --git a/packages/utils/.gitignore b/packages/utils/.gitignore new file mode 100644 index 0000000000..763301fc00 --- /dev/null +++ b/packages/utils/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 0000000000..270940f522 --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,3 @@ +# Utility package + +Those are generic utility functions that can be imported from any other packages. This package however shouldn't have any dependency to any other Joplin package, so that it can be imported without having to import "lib", "renderer" and so on. This is particulary important for Docker images, so that they can be trimmed down to only what's needed. \ No newline at end of file diff --git a/packages/utils/commandToString.ts b/packages/utils/commandToString.ts new file mode 100644 index 0000000000..3cc1f854f8 --- /dev/null +++ b/packages/utils/commandToString.ts @@ -0,0 +1,16 @@ +const quotePath = (path: string) => { + if (!path) return ''; + if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path; + path = path.replace(/"/, '\\"'); + return `"${path}"`; +}; + +export default (commandName: string, args: string[] = []) => { + const output = [quotePath(commandName)]; + + for (const arg of args) { + output.push(quotePath(arg)); + } + + return output.join(' '); +}; diff --git a/packages/utils/execCommand.ts b/packages/utils/execCommand.ts new file mode 100644 index 0000000000..40415d87c2 --- /dev/null +++ b/packages/utils/execCommand.ts @@ -0,0 +1,44 @@ +import * as execa from 'execa'; +import commandToString from './commandToString'; +import splitCommandString from './splitCommandString'; +import { stdout } from 'process'; + +interface ExecCommandOptions { + showInput?: boolean; + showStdout?: boolean; + showStderr?: boolean; + quiet?: boolean; +} + +export default async (command: string | string[], options: ExecCommandOptions | null = null): Promise => { + options = { + showInput: true, + showStdout: true, + showStderr: true, + quiet: false, + ...options, + }; + + if (options.quiet) { + options.showInput = false; + options.showStdout = false; + options.showStderr = false; + } + + if (options.showInput) { + if (typeof command === 'string') { + stdout.write(`> ${command}\n`); + } else { + stdout.write(`> ${commandToString(command[0], command.slice(1))}\n`); + } + } + + 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.showStdout && promise.stdout) promise.stdout.pipe(process.stdout); + if (options.showStderr && promise.stdout) promise.stdout.pipe(process.stderr); + const result = await promise; + return result.stdout.trim(); +}; diff --git a/packages/utils/index.ts b/packages/utils/index.ts new file mode 100644 index 0000000000..1a2ef395e5 --- /dev/null +++ b/packages/utils/index.ts @@ -0,0 +1,9 @@ +import execCommand from './execCommand'; +import commandToString from './commandToString'; +import splitCommandString from './splitCommandString'; + +export { + execCommand, + commandToString, + splitCommandString, +}; diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..4c592fd281 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,21 @@ +{ + "name": "@joplin/utils", + "version": "2.10.0", + "description": "Utilities for Joplin", + "repository": "https://github.com/laurent22/joplin/tree/dev/packages/utils", + "main": "dist/index.js", + "publishConfig": { + "access": "public" + }, + "scripts": { + "tsc": "tsc --project tsconfig.json", + "watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", + "test": "jest --verbose=false", + "test-ci": "yarn test" + }, + "author": "", + "license": "AGPL-3.0-or-later", + "dependencies": { + "execa": "5.1.1" + } +} diff --git a/packages/utils/splitCommandString.ts b/packages/utils/splitCommandString.ts new file mode 100644 index 0000000000..cdaab6cd47 --- /dev/null +++ b/packages/utils/splitCommandString.ts @@ -0,0 +1,73 @@ +export default (command: string, options: any = 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/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..141d57b761 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "strict": true, + "strictNullChecks": true + }, + "rootDir": ".", + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "**/node_modules", + ], +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8c4ac939c0..8544824fd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4880,6 +4880,7 @@ __metadata: "@joplin/renderer": ^2.10.2 "@joplin/turndown": ^4.0.65 "@joplin/turndown-plugin-gfm": ^1.0.47 + "@joplin/utils": ~2.10 "@types/fs-extra": 9.0.13 "@types/jest": 29.2.6 "@types/js-yaml": 4.0.5 @@ -4985,6 +4986,7 @@ __metadata: dependencies: "@joplin/lib": ^2.10.2 "@joplin/tools": ^2.10.2 + "@joplin/utils": ~2.10 "@types/fs-extra": 9.0.13 "@types/jest": 29.2.6 "@types/node": 18.11.18 @@ -5138,6 +5140,7 @@ __metadata: "@joplin/fork-htmlparser2": ^4.1.43 "@joplin/lib": ^2.10.2 "@joplin/renderer": ^2.10.2 + "@joplin/utils": ~2.10 "@rmp135/sql-ts": 1.16.0 "@types/fs-extra": 9.0.13 "@types/jest": 29.2.6 @@ -5201,6 +5204,14 @@ __metadata: languageName: unknown linkType: soft +"@joplin/utils@workspace:packages/utils, @joplin/utils@~2.10": + version: 0.0.0-use.local + resolution: "@joplin/utils@workspace:packages/utils" + dependencies: + execa: 5.1.1 + languageName: unknown + linkType: soft + "@jridgewell/gen-mapping@npm:^0.1.0": version: 0.1.1 resolution: "@jridgewell/gen-mapping@npm:0.1.1" @@ -15681,22 +15692,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^1.0.0": - version: 1.0.0 - resolution: "execa@npm:1.0.0" - dependencies: - cross-spawn: ^6.0.0 - get-stream: ^4.0.0 - is-stream: ^1.1.0 - npm-run-path: ^2.0.0 - p-finally: ^1.0.0 - signal-exit: ^3.0.0 - strip-eof: ^1.0.0 - checksum: ddf1342c1c7d02dd93b41364cd847640f6163350d9439071abf70bf4ceb1b9b2b2e37f54babb1d8dc1df8e0d8def32d0e81e74a2e62c3e1d70c303eb4c306bc4 - languageName: node - linkType: hard - -"execa@npm:^5.0.0, execa@npm:^5.1.1": +"execa@npm:5.1.1, execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -15713,6 +15709,21 @@ __metadata: languageName: node linkType: hard +"execa@npm:^1.0.0": + version: 1.0.0 + resolution: "execa@npm:1.0.0" + dependencies: + cross-spawn: ^6.0.0 + get-stream: ^4.0.0 + is-stream: ^1.1.0 + npm-run-path: ^2.0.0 + p-finally: ^1.0.0 + signal-exit: ^3.0.0 + strip-eof: ^1.0.0 + checksum: ddf1342c1c7d02dd93b41364cd847640f6163350d9439071abf70bf4ceb1b9b2b2e37f54babb1d8dc1df8e0d8def32d0e81e74a2e62c3e1d70c303eb4c306bc4 + languageName: node + linkType: hard + "execa@npm:^7.0.0": version: 7.1.1 resolution: "execa@npm:7.1.1" @@ -20444,6 +20455,7 @@ __metadata: "@joplin/lib": ~2.10 "@joplin/renderer": ~2.10 "@joplin/tools": ~2.10 + "@joplin/utils": ~2.10 "@types/fs-extra": 9.0.13 "@types/jest": 29.2.6 "@types/node": 18.11.18