You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Plugins: Updated plugin repo script
This commit is contained in:
		| @@ -6,4 +6,5 @@ packages/app-desktop | ||||
| packages/app-cli | ||||
| packages/app-mobile | ||||
| packages/app-clipper | ||||
| packages/generator-joplin | ||||
| packages/generator-joplin | ||||
| packages/plugin-repo-cli | ||||
| @@ -1371,6 +1371,12 @@ packages/lib/uuid.js.map | ||||
| packages/lib/versionInfo.d.ts | ||||
| packages/lib/versionInfo.js | ||||
| packages/lib/versionInfo.js.map | ||||
| packages/plugin-repo-cli/dummy.test.d.ts | ||||
| packages/plugin-repo-cli/dummy.test.js | ||||
| packages/plugin-repo-cli/dummy.test.js.map | ||||
| packages/plugin-repo-cli/index.d.ts | ||||
| packages/plugin-repo-cli/index.js | ||||
| packages/plugin-repo-cli/index.js.map | ||||
| packages/plugins/ToggleSidebars/api/index.d.ts | ||||
| packages/plugins/ToggleSidebars/api/index.js | ||||
| packages/plugins/ToggleSidebars/api/index.js.map | ||||
| @@ -1461,21 +1467,6 @@ packages/renderer/utils.js.map | ||||
| packages/server/src/app.d.ts | ||||
| packages/server/src/app.js | ||||
| packages/server/src/app.js.map | ||||
| packages/server/src/config-base.d.ts | ||||
| packages/server/src/config-base.js | ||||
| packages/server/src/config-base.js.map | ||||
| packages/server/src/config-buildTypes.d.ts | ||||
| packages/server/src/config-buildTypes.js | ||||
| packages/server/src/config-buildTypes.js.map | ||||
| packages/server/src/config-dev.d.ts | ||||
| packages/server/src/config-dev.js | ||||
| packages/server/src/config-dev.js.map | ||||
| packages/server/src/config-prod.d.ts | ||||
| packages/server/src/config-prod.js | ||||
| packages/server/src/config-prod.js.map | ||||
| packages/server/src/config-tests.d.ts | ||||
| packages/server/src/config-tests.js | ||||
| packages/server/src/config-tests.js.map | ||||
| packages/server/src/config.d.ts | ||||
| packages/server/src/config.js | ||||
| packages/server/src/config.js.map | ||||
| @@ -1686,10 +1677,13 @@ packages/server/src/utils/urlUtils.js.map | ||||
| packages/server/src/utils/uuidgen.d.ts | ||||
| packages/server/src/utils/uuidgen.js | ||||
| packages/server/src/utils/uuidgen.js.map | ||||
| packages/tools/build-plugin-repository.d.ts | ||||
| packages/tools/build-plugin-repository.js | ||||
| packages/tools/build-plugin-repository.js.map | ||||
| packages/tools/lerna-add.d.ts | ||||
| packages/tools/lerna-add.js | ||||
| packages/tools/lerna-add.js.map | ||||
| packages/tools/release-server.d.ts | ||||
| packages/tools/release-server.js | ||||
| packages/tools/release-server.js.map | ||||
| packages/tools/tool-utils.d.ts | ||||
| packages/tools/tool-utils.js | ||||
| packages/tools/tool-utils.js.map | ||||
| # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD | ||||
|   | ||||
							
								
								
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1359,6 +1359,12 @@ packages/lib/uuid.js.map | ||||
| packages/lib/versionInfo.d.ts | ||||
| packages/lib/versionInfo.js | ||||
| packages/lib/versionInfo.js.map | ||||
| packages/plugin-repo-cli/dummy.test.d.ts | ||||
| packages/plugin-repo-cli/dummy.test.js | ||||
| packages/plugin-repo-cli/dummy.test.js.map | ||||
| packages/plugin-repo-cli/index.d.ts | ||||
| packages/plugin-repo-cli/index.js | ||||
| packages/plugin-repo-cli/index.js.map | ||||
| packages/plugins/ToggleSidebars/api/index.d.ts | ||||
| packages/plugins/ToggleSidebars/api/index.js | ||||
| packages/plugins/ToggleSidebars/api/index.js.map | ||||
| @@ -1449,21 +1455,6 @@ packages/renderer/utils.js.map | ||||
| packages/server/src/app.d.ts | ||||
| packages/server/src/app.js | ||||
| packages/server/src/app.js.map | ||||
| packages/server/src/config-base.d.ts | ||||
| packages/server/src/config-base.js | ||||
| packages/server/src/config-base.js.map | ||||
| packages/server/src/config-buildTypes.d.ts | ||||
| packages/server/src/config-buildTypes.js | ||||
| packages/server/src/config-buildTypes.js.map | ||||
| packages/server/src/config-dev.d.ts | ||||
| packages/server/src/config-dev.js | ||||
| packages/server/src/config-dev.js.map | ||||
| packages/server/src/config-prod.d.ts | ||||
| packages/server/src/config-prod.js | ||||
| packages/server/src/config-prod.js.map | ||||
| packages/server/src/config-tests.d.ts | ||||
| packages/server/src/config-tests.js | ||||
| packages/server/src/config-tests.js.map | ||||
| packages/server/src/config.d.ts | ||||
| packages/server/src/config.js | ||||
| packages/server/src/config.js.map | ||||
| @@ -1674,10 +1665,13 @@ packages/server/src/utils/urlUtils.js.map | ||||
| packages/server/src/utils/uuidgen.d.ts | ||||
| packages/server/src/utils/uuidgen.js | ||||
| packages/server/src/utils/uuidgen.js.map | ||||
| packages/tools/build-plugin-repository.d.ts | ||||
| packages/tools/build-plugin-repository.js | ||||
| packages/tools/build-plugin-repository.js.map | ||||
| packages/tools/lerna-add.d.ts | ||||
| packages/tools/lerna-add.js | ||||
| packages/tools/lerna-add.js.map | ||||
| packages/tools/release-server.d.ts | ||||
| packages/tools/release-server.js | ||||
| packages/tools/release-server.js.map | ||||
| packages/tools/tool-utils.d.ts | ||||
| packages/tools/tool-utils.js | ||||
| packages/tools/tool-utils.js.map | ||||
| # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|     "buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md", | ||||
|     "buildDoc": "./packages/tools/build-all.sh", | ||||
|     "buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/", | ||||
|     "buildPluginRepo": "node packages/tools/build-plugin-repository.js", | ||||
|     "buildTranslations": "npm run tsc && node packages/tools/build-translation.js", | ||||
|     "buildTranslationsNoTsc": "node packages/tools/build-translation.js", | ||||
|     "buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc", | ||||
|   | ||||
| @@ -52,7 +52,7 @@ | ||||
|         "coveralls": "^3.0.1", | ||||
|         "eslint": "^6.0.0", | ||||
|         "eslint-config-prettier": "^6.0.0", | ||||
|         "jest": "^24.8.0", | ||||
|         "jest": "^26.6.3", | ||||
|         "prettier": "^1.18.2", | ||||
|         "ts-jest": "^24.0.2", | ||||
|         "typescript": "^3.5.3" | ||||
|   | ||||
							
								
								
									
										9
									
								
								packages/plugin-repo-cli/dummy.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/plugin-repo-cli/dummy.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| // Dummy test because the Jest setup is done but there's for now no test. | ||||
|  | ||||
| describe('dummy', () => { | ||||
|  | ||||
| 	it('should pass', () => { | ||||
| 		expect(1).toBe(1); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
| @@ -3,7 +3,7 @@ import * as path from 'path'; | ||||
| import * as process from 'process'; | ||||
| import validatePluginId from '@joplin/lib/services/plugins/utils/validatePluginId'; | ||||
| import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '@joplin/lib/markdownUtils'; | ||||
| const { execCommand, execCommandVerbose, rootDir, resolveRelativePathWithinDir, gitPullTry } = require('./tool-utils.js'); | ||||
| import { execCommand2, resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js'; | ||||
| 
 | ||||
| interface NpmPackage { | ||||
| 	name: string; | ||||
| @@ -45,6 +45,7 @@ async function checkPluginRepository(dirPath: string) { | ||||
| 
 | ||||
| 	const previousDir = process.cwd(); | ||||
| 	process.chdir(dirPath); | ||||
| 	await gitRepoCleanTry(); | ||||
| 	await gitPullTry(); | ||||
| 	process.chdir(previousDir); | ||||
| } | ||||
| @@ -70,13 +71,13 @@ async function extractPluginFilesFromPackage(existingManifests: any, workDir: st | ||||
| 	const previousDir = process.cwd(); | ||||
| 	process.chdir(workDir); | ||||
| 
 | ||||
| 	await execCommandVerbose('npm', ['install', packageName, '--save', '--ignore-scripts']); | ||||
| 	await execCommand2(`npm install ${packageName} --save --ignore-scripts`, { showOutput: false }); | ||||
| 
 | ||||
| 	const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish'); | ||||
| 
 | ||||
| 	const files = await fs.readdir(pluginDir); | ||||
| 	const manifestFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.json')); | ||||
| 	const pluginFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.jpl')); | ||||
| 	const manifestFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.json')); | ||||
| 	const pluginFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.jpl')); | ||||
| 
 | ||||
| 	if (!(await fs.pathExists(manifestFilePath))) throw new Error(`Could not find manifest file at ${manifestFilePath}`); | ||||
| 	if (!(await fs.pathExists(pluginFilePath))) throw new Error(`Could not find plugin file at ${pluginFilePath}`); | ||||
| @@ -155,23 +156,41 @@ async function updateReadme(readmePath: string, manifests: any) { | ||||
| 
 | ||||
| 	const tableRegex = /<!-- PLUGIN_LIST -->([^]*)<!-- PLUGIN_LIST -->/; | ||||
| 
 | ||||
| 	const content = await fs.readFile(readmePath, 'utf8'); | ||||
| 	const content = await fs.pathExists(readmePath) ? await fs.readFile(readmePath, 'utf8') : '<!-- PLUGIN_LIST -->\n<!-- PLUGIN_LIST -->'; | ||||
| 	const newContent = content.replace(tableRegex, `<!-- PLUGIN_LIST -->\n${mdTable}\n<!-- PLUGIN_LIST -->`); | ||||
| 
 | ||||
| 	await fs.writeFile(readmePath, newContent, 'utf8'); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
| 	// We assume that the repository is located in a directory next to the main
 | ||||
| 	// Joplin monorepo.
 | ||||
| 	const repoDir = path.resolve(path.dirname(rootDir), 'joplin-plugins'); | ||||
| interface CommandBuildArgs { | ||||
| 	pluginRepoDir: string; | ||||
| } | ||||
| 
 | ||||
| enum ProcessingActionType { | ||||
| 	Add = 1, | ||||
| 	Update = 2, | ||||
| } | ||||
| 
 | ||||
| function commitMessage(actionType: ProcessingActionType, npmPackage: NpmPackage): string { | ||||
| 	const output: string[] = []; | ||||
| 
 | ||||
| 	if (actionType === ProcessingActionType.Add) { | ||||
| 		output.push('New'); | ||||
| 	} else { | ||||
| 		output.push('Update'); | ||||
| 	} | ||||
| 
 | ||||
| 	output.push(`${npmPackage.name}@${npmPackage.version}`); | ||||
| 
 | ||||
| 	return output.join(': '); | ||||
| } | ||||
| 
 | ||||
| async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) { | ||||
| 	const tempDir = `${repoDir}/temp`; | ||||
| 	const pluginManifestsPath = path.resolve(repoDir, 'manifests.json'); | ||||
| 	const obsoleteManifestsPath = path.resolve(repoDir, 'obsoletes.json'); | ||||
| 	const errorsPath = path.resolve(repoDir, 'errors.json'); | ||||
| 
 | ||||
| 	await checkPluginRepository(repoDir); | ||||
| 
 | ||||
| 	await fs.mkdirp(tempDir); | ||||
| 
 | ||||
| 	const originalPluginManifests = await readJsonFile(pluginManifestsPath, {}); | ||||
| @@ -181,29 +200,31 @@ async function main() { | ||||
| 		...obsoleteManifests, | ||||
| 	}; | ||||
| 
 | ||||
| 	const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json')).trim(); | ||||
| 	const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults)); | ||||
| 
 | ||||
| 	const packageTempDir = `${tempDir}/packages`; | ||||
| 
 | ||||
| 	await fs.mkdirp(packageTempDir); | ||||
| 	const previousDir = process.cwd(); | ||||
| 	process.chdir(packageTempDir); | ||||
| 	await execCommand('npm init --yes --loglevel silent'); | ||||
| 	await execCommand2('npm init --yes --loglevel silent', { quiet: true }); | ||||
| 
 | ||||
| 	const errors: any[] = []; | ||||
| 	const errors: any = await readJsonFile(errorsPath, {}); | ||||
| 	delete errors[npmPackage.name]; | ||||
| 
 | ||||
| 	let actionType: ProcessingActionType = ProcessingActionType.Update; | ||||
| 	let manifests: any = {}; | ||||
| 
 | ||||
| 	for (const npmPackage of npmPackages) { | ||||
| 		try { | ||||
| 			const packageName = npmPackage.name; | ||||
| 			const destDir = `${repoDir}/plugins/`; | ||||
| 			const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, packageName, destDir); | ||||
| 			if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest; | ||||
| 		} catch (error) { | ||||
| 			console.error(error); | ||||
| 			errors.push(error); | ||||
| 	try { | ||||
| 		const destDir = `${repoDir}/plugins/`; | ||||
| 		const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, npmPackage.name, destDir); | ||||
| 
 | ||||
| 		if (!existingManifests[manifest.id]) { | ||||
| 			actionType = ProcessingActionType.Add; | ||||
| 		} | ||||
| 
 | ||||
| 		if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest; | ||||
| 	} catch (error) { | ||||
| 		console.error(error); | ||||
| 		errors[npmPackage.name] = error.message || ''; | ||||
| 	} | ||||
| 
 | ||||
| 	// We preserve the original manifests so that if a plugin has been removed
 | ||||
| @@ -217,20 +238,79 @@ async function main() { | ||||
| 
 | ||||
| 	await fs.writeFile(pluginManifestsPath, JSON.stringify(manifests, null, '\t'), 'utf8'); | ||||
| 
 | ||||
| 	if (errors.length) { | ||||
| 		const toWrite = errors.map((e: any) => { | ||||
| 			return { | ||||
| 				message: e.message || '', | ||||
| 			}; | ||||
| 		}); | ||||
| 		await fs.writeFile(errorsPath, JSON.stringify(toWrite, null, '\t'), 'utf8'); | ||||
| 	if (Object.keys(errors).length) { | ||||
| 		await fs.writeFile(errorsPath, JSON.stringify(errors, null, '\t'), 'utf8'); | ||||
| 	} else { | ||||
| 		await fs.remove(errorsPath); | ||||
| 	} | ||||
| 
 | ||||
| 	await updateReadme(`${repoDir}/README.md`, manifests); | ||||
| 
 | ||||
| 	process.chdir(previousDir); | ||||
| 	await fs.remove(tempDir); | ||||
| 
 | ||||
| 	process.chdir(repoDir); | ||||
| 
 | ||||
| 	if (!(await gitRepoClean())) { | ||||
| 		await execCommand2('git add -A', { showOutput: false }); | ||||
| 		await execCommand2(['git', 'commit', '-m', commitMessage(actionType, npmPackage)], { showOutput: false }); | ||||
| 	} else { | ||||
| 		console.info('Nothing to commit'); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| async function commandBuild(args: CommandBuildArgs) { | ||||
| 	const repoDir = args.pluginRepoDir; | ||||
| 	await checkPluginRepository(repoDir); | ||||
| 
 | ||||
| 	const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showOutput: false })).trim(); | ||||
| 	const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults)); | ||||
| 
 | ||||
| 	for (const npmPackage of npmPackages) { | ||||
| 		await processNpmPackage(npmPackage, repoDir); | ||||
| 	} | ||||
| 
 | ||||
| 	await execCommand2('git push'); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
| 	const scriptName: string = 'plugin-repo-cli'; | ||||
| 
 | ||||
| 	const commands: Record<string, Function> = { | ||||
| 		build: commandBuild, | ||||
| 	}; | ||||
| 
 | ||||
| 	let selectedCommand: string = ''; | ||||
| 	let selectedCommandArgs: string = ''; | ||||
| 
 | ||||
| 	function setSelectedCommand(name: string, args: any) { | ||||
| 		selectedCommand = name; | ||||
| 		selectedCommandArgs = args; | ||||
| 	} | ||||
| 
 | ||||
| 	require('yargs') | ||||
| 		.scriptName(scriptName) | ||||
| 		.usage('$0 <cmd> [args]') | ||||
| 		.command('build <plugin-repo-dir>', 'Build the plugin repository', (yargs: any) => { | ||||
| 			yargs.positional('plugin-repo-dir', { | ||||
| 				type: 'string', | ||||
| 				describe: 'Directory where the plugin repository is located', | ||||
| 			}); | ||||
| 		}, (args: any) => setSelectedCommand('build', args)) | ||||
| 		.help() | ||||
| 		.argv; | ||||
| 
 | ||||
| 	if (!selectedCommand) { | ||||
| 		console.error(`Please provide a command name or type \`${scriptName} --help\` for help`); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!commands[selectedCommand]) { | ||||
| 		console.error(`No such command: ${selectedCommand}`); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 
 | ||||
| 	await commands[selectedCommand](selectedCommandArgs); | ||||
| } | ||||
| 
 | ||||
| main().catch((error) => { | ||||
							
								
								
									
										4894
									
								
								packages/plugin-repo-cli/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4894
									
								
								packages/plugin-repo-cli/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										31
									
								
								packages/plugin-repo-cli/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/plugin-repo-cli/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| { | ||||
|   "name": "@joplin/plugin-repo-builder", | ||||
|   "version": "1.7.0", | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "publishConfig": { | ||||
|     "access": "public" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "tsc": "tsc --project tsconfig.json", | ||||
|     "watch": "tsc --watch --project tsconfig.json", | ||||
|     "prepare": "npm run tsc", | ||||
|     "test": "jest", | ||||
|     "test-ci": "npm run test" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@joplin/lib": "*", | ||||
|     "@joplin/tools": "*", | ||||
|     "fs-extra": "^9.0.1", | ||||
|     "yargs": "^16.0.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/jest": "^26.0.15", | ||||
|     "@types/node": "^14.14.6", | ||||
|     "@types/fs-extra": "^9.0.6", | ||||
|     "jest": "^26.6.3", | ||||
|     "typescript": "^4.1.3" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								packages/plugin-repo-cli/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/plugin-repo-cli/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| { | ||||
| 	"extends": "../../tsconfig.json", | ||||
| 	"include": [ | ||||
| 		"**/*.ts", | ||||
| 		"**/*.tsx", | ||||
| 	], | ||||
| 	"exclude": [ | ||||
| 		"**/node_modules", | ||||
| 	], | ||||
| } | ||||
							
								
								
									
										40
									
								
								packages/tools/lerna-add.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								packages/tools/lerna-add.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // // npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder | ||||
|  | ||||
| // import { execCommand2, rootDir, gitPullTry } from './tool-utils.js'; | ||||
|  | ||||
| // async function main() { | ||||
| // 	const argv = require('yargs').argv; | ||||
|  | ||||
| // 	console.info(process.argv); | ||||
|  | ||||
| // 	const args = []; | ||||
| // 	if (argv.D) args.push('-D'); | ||||
|  | ||||
| // 	await execCommand2('npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest'); | ||||
|  | ||||
| // 	//npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder | ||||
| // 	// process.chdir(rootDir); | ||||
| // 	// const version = (await execCommand2('npm version patch')).trim(); | ||||
| // 	// const versionShort = version.substr(1); | ||||
| // 	// const tagName = `server-${version}`; | ||||
|  | ||||
| // 	// process.chdir(rootDir); | ||||
| // 	// console.info(`Running from: ${process.cwd()}`); | ||||
|  | ||||
| // 	// await execCommand2(`docker build -t "joplin/server:${versionShort}" -f Dockerfile.server .`); | ||||
| // 	// await execCommand2(`docker tag "joplin/server:${versionShort}" "joplin/server:latest"`); | ||||
| // 	// await execCommand2(`docker push joplin/server:${versionShort}`); | ||||
| // 	// await execCommand2('docker push joplin/server:latest'); | ||||
|  | ||||
| // 	// await execCommand2('git add -A'); | ||||
| // 	// await execCommand2(`git commit -m 'Server release ${version}'`); | ||||
| // 	// await execCommand2(`git tag ${tagName}`); | ||||
| // 	// await execCommand2('git push'); | ||||
| // 	// await execCommand2('git push --tags'); | ||||
| // } | ||||
|  | ||||
| // main().catch((error) => { | ||||
| // 	console.error('Fatal error'); | ||||
| // 	console.error(error); | ||||
| // 	process.exit(1); | ||||
| // }); | ||||
							
								
								
									
										3336
									
								
								packages/tools/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3336
									
								
								packages/tools/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,4 +1,4 @@ | ||||
| const { execCommand2, rootDir, gitPullTry } = require('./tool-utils.js'); | ||||
| import { execCommand2, rootDir, gitPullTry } from './tool-utils'; | ||||
|  | ||||
| const serverDir = `${rootDir}/packages/server`; | ||||
|  | ||||
|   | ||||
| @@ -80,6 +80,7 @@ async function main() { | ||||
| 	await updatePackageVersion(`${rootDir}/packages/app-cli/package.json`, majorMinorVersion); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/generator-joplin/package.json`, majorMinorVersion); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/server/package.json`, majorMinorVersion); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/plugin-repo-cli/package.json`, majorMinorVersion); | ||||
| 	await updateGradleVersion(`${rootDir}/packages/app-mobile/android/app/build.gradle`, majorMinorVersion); | ||||
| 	await updateCodeProjVersion(`${rootDir}/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj`, iosVersionHack(majorMinorVersion)); | ||||
| 	await updateClipperManifestVersion(`${rootDir}/packages/app-clipper/manifest.json`, majorMinorVersion); | ||||
|   | ||||
| @@ -1,16 +1,51 @@ | ||||
| import * as fs from 'fs-extra'; | ||||
| import { execSync } from 'child_process'; | ||||
| 
 | ||||
| const fetch = require('node-fetch'); | ||||
| const fs = require('fs-extra'); | ||||
| const execa = require('execa'); | ||||
| const { execSync } = require('child_process'); | ||||
| const { splitCommandString } = require('@joplin/lib/string-utils'); | ||||
| 
 | ||||
| const toolUtils = {}; | ||||
| function quotePath(path: string) { | ||||
| 	if (!path) return ''; | ||||
| 	if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path; | ||||
| 	path = path.replace(/"/, '\\"'); | ||||
| 	return `"${path}"`; | ||||
| } | ||||
| 
 | ||||
| toolUtils.execCommand = function(command) { | ||||
| function commandToString(commandName: string, args: string[] = []) { | ||||
| 	const output = [quotePath(commandName)]; | ||||
| 
 | ||||
| 	for (const arg of args) { | ||||
| 		output.push(quotePath(arg)); | ||||
| 	} | ||||
| 
 | ||||
| 	return output.join(' '); | ||||
| } | ||||
| 
 | ||||
| async function loadGitHubUsernameCache() { | ||||
| 	const path = `${__dirname}/github_username_cache.json`; | ||||
| 
 | ||||
| 	if (await fs.pathExists(path)) { | ||||
| 		const jsonString = await fs.readFile(path, 'utf8'); | ||||
| 		return JSON.parse(jsonString); | ||||
| 	} | ||||
| 
 | ||||
| 	return {}; | ||||
| } | ||||
| 
 | ||||
| async function saveGitHubUsernameCache(cache: any) { | ||||
| 	const path = `${__dirname}/github_username_cache.json`; | ||||
| 	await fs.writeFile(path, JSON.stringify(cache)); | ||||
| } | ||||
| 
 | ||||
| // Returns the project root dir
 | ||||
| export const rootDir = require('path').dirname(require('path').dirname(__dirname)); | ||||
| 
 | ||||
| export function execCommand(command: string) { | ||||
| 	const exec = require('child_process').exec; | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		exec(command, (error, stdout, stderr) => { | ||||
| 		exec(command, (error: any, stdout: any, stderr: any) => { | ||||
| 			if (error) { | ||||
| 				if (error.signal == 'SIGTERM') { | ||||
| 					resolve('Process was killed'); | ||||
| @@ -22,39 +57,28 @@ toolUtils.execCommand = function(command) { | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| function quotePath(path) { | ||||
| 	if (!path) return ''; | ||||
| 	if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path; | ||||
| 	path = path.replace(/"/, '\\"'); | ||||
| 	return `"${path}"`; | ||||
| } | ||||
| 
 | ||||
| function commandToString(commandName, args = []) { | ||||
| 	const output = [quotePath(commandName)]; | ||||
| 
 | ||||
| 	for (const arg of args) { | ||||
| 		output.push(quotePath(arg)); | ||||
| 	} | ||||
| 
 | ||||
| 	return output.join(' '); | ||||
| } | ||||
| 
 | ||||
| toolUtils.resolveRelativePathWithinDir = function(baseDir, ...relativePath) { | ||||
| export function resolveRelativePathWithinDir(baseDir: string, ...relativePath: string[]) { | ||||
| 	const path = require('path'); | ||||
| 	const resolvedBaseDir = path.resolve(baseDir); | ||||
| 	const resolvedPath = path.resolve(baseDir, ...relativePath); | ||||
| 	if (resolvedPath.indexOf(resolvedBaseDir) !== 0) throw new Error(`Resolved path for relative path "${JSON.stringify(relativePath)}" is not within base directory "${baseDir}" (Was resolved to ${resolvedPath})`); | ||||
| 	return resolvedPath; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.execCommandVerbose = function(commandName, args = []) { | ||||
| export function execCommandVerbose(commandName: string, args: string[] = []) { | ||||
| 	console.info(`> ${commandToString(commandName, args)}`); | ||||
| 	const promise = execa(commandName, args); | ||||
| 	promise.stdout.pipe(process.stdout); | ||||
| 	return promise; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| interface ExecCommandOptions { | ||||
| 	showInput?: boolean; | ||||
| 	showOutput?: boolean; | ||||
| 	quiet?: boolean; | ||||
| } | ||||
| 
 | ||||
| // There's lot of execCommandXXX functions, but eventually all scripts should
 | ||||
| // use the one below, which supports:
 | ||||
| @@ -62,66 +86,79 @@ toolUtils.execCommandVerbose = function(commandName, args = []) { | ||||
| // - Printing the command being executed
 | ||||
| // - Printing the output in real time (piping to stdout)
 | ||||
| // - Returning the command result as string
 | ||||
| toolUtils.execCommand2 = async function(command, options = null) { | ||||
| export async function execCommand2(command: string | string[], options: ExecCommandOptions = null): Promise<string> { | ||||
| 	options = { | ||||
| 		showInput: true, | ||||
| 		showOutput: true, | ||||
| 		quiet: false, | ||||
| 		...options, | ||||
| 	}; | ||||
| 
 | ||||
| 	if (options.showInput) console.info(`> ${command}`); | ||||
| 	const args = splitCommandString(command); | ||||
| 	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; | ||||
| }; | ||||
| 	return result.stdout.trim(); | ||||
| } | ||||
| 
 | ||||
| toolUtils.execCommandWithPipes = function(executable, args) { | ||||
| export function execCommandWithPipes(executable: string, args: string[]) { | ||||
| 	const spawn = require('child_process').spawn; | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		const child = spawn(executable, args, { stdio: 'inherit' }); | ||||
| 
 | ||||
| 		child.on('error', (error) => { | ||||
| 		child.on('error', (error: any) => { | ||||
| 			reject(error); | ||||
| 		}); | ||||
| 
 | ||||
| 		child.on('close', (code) => { | ||||
| 		child.on('close', (code: any) => { | ||||
| 			if (code !== 0) { | ||||
| 				reject(`Ended with code ${code}`); | ||||
| 			} else { | ||||
| 				resolve(); | ||||
| 				resolve(null); | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.toSystemSlashes = function(path) { | ||||
| export function toSystemSlashes(path: string) { | ||||
| 	const os = process.platform; | ||||
| 	if (os === 'win32') return path.replace(/\//g, '\\'); | ||||
| 	return path.replace(/\\/g, '/'); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.deleteLink = async function(path) { | ||||
| 	if (toolUtils.isWindows()) { | ||||
| export function deleteLink(path: string) { | ||||
| 	if (isWindows()) { | ||||
| 		try { | ||||
| 			execSync(`rmdir "${toolUtils.toSystemSlashes(path)}"`, { stdio: 'pipe' }); | ||||
| 			execSync(`rmdir "${toSystemSlashes(path)}"`, { stdio: 'pipe' }); | ||||
| 		} catch (error) { | ||||
| 			// console.info('Error: ' + error.message);
 | ||||
| 		} | ||||
| 	} else { | ||||
| 		try { | ||||
| 			fs.unlinkSync(toolUtils.toSystemSlashes(path)); | ||||
| 			fs.unlinkSync(toSystemSlashes(path)); | ||||
| 		} catch (error) { | ||||
| 			// ignore
 | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.setPackagePrivateField = async function(filePath, value) { | ||||
| export async function setPackagePrivateField(filePath: string, value: any) { | ||||
| 	const text = await fs.readFile(filePath, 'utf8'); | ||||
| 	const obj = JSON.parse(text); | ||||
| 	if (!value) { | ||||
| @@ -130,9 +167,9 @@ toolUtils.setPackagePrivateField = async function(filePath, value) { | ||||
| 		obj.private = true; | ||||
| 	} | ||||
| 	await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8'); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.credentialDir = async function() { | ||||
| export async function credentialDir() { | ||||
| 	const username = require('os').userInfo().username; | ||||
| 
 | ||||
| 	const toTry = [ | ||||
| @@ -143,48 +180,45 @@ toolUtils.credentialDir = async function() { | ||||
| 	]; | ||||
| 
 | ||||
| 	for (const dirPath of toTry) { | ||||
| 		if (await fs.exists(dirPath)) return dirPath; | ||||
| 		if (await fs.pathExists(dirPath)) return dirPath; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new Error(`Could not find credential directory in any of these paths: ${JSON.stringify(toTry)}`); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| // Returns the project root dir
 | ||||
| toolUtils.rootDir = require('path').dirname(require('path').dirname(__dirname)); | ||||
| 
 | ||||
| toolUtils.credentialFile = async function(filename) { | ||||
| 	const rootDir = await toolUtils.credentialDir(); | ||||
| export async function credentialFile(filename: string) { | ||||
| 	const rootDir = await credentialDir(); | ||||
| 	const output = `${rootDir}/${filename}`; | ||||
| 	if (!(await fs.exists(output))) throw new Error(`No such file: ${output}`); | ||||
| 	if (!(await fs.pathExists(output))) throw new Error(`No such file: ${output}`); | ||||
| 	return output; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.readCredentialFile = async function(filename) { | ||||
| 	const filePath = await toolUtils.credentialFile(filename); | ||||
| export async function readCredentialFile(filename: string) { | ||||
| 	const filePath = await credentialFile(filename); | ||||
| 	const r = await fs.readFile(filePath); | ||||
| 	return r.toString(); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.downloadFile = function(url, targetPath) { | ||||
| export async function downloadFile(url: string, targetPath: string) { | ||||
| 	const https = require('https'); | ||||
| 	const fs = require('fs'); | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		const file = fs.createWriteStream(targetPath); | ||||
| 		https.get(url, function(response) { | ||||
| 		https.get(url, function(response: any) { | ||||
| 			if (response.statusCode !== 200) reject(new Error(`HTTP error ${response.statusCode}`)); | ||||
| 			response.pipe(file); | ||||
| 			file.on('finish', function() { | ||||
| 				// file.close();
 | ||||
| 				resolve(); | ||||
| 				resolve(null); | ||||
| 			}); | ||||
| 		}).on('error', (error) => { | ||||
| 		}).on('error', (error: any) => { | ||||
| 			reject(error); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.fileSha256 = function(filePath) { | ||||
| export function fileSha256(filePath: string) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		const crypto = require('crypto'); | ||||
| 		const fs = require('fs'); | ||||
| @@ -192,18 +226,18 @@ toolUtils.fileSha256 = function(filePath) { | ||||
| 		const shasum = crypto.createHash(algo); | ||||
| 
 | ||||
| 		const s = fs.ReadStream(filePath); | ||||
| 		s.on('data', function(d) { shasum.update(d); }); | ||||
| 		s.on('data', function(d: any) { shasum.update(d); }); | ||||
| 		s.on('end', function() { | ||||
| 			const d = shasum.digest('hex'); | ||||
| 			resolve(d); | ||||
| 		}); | ||||
| 		s.on('error', function(error) { | ||||
| 		s.on('error', function(error: any) { | ||||
| 			reject(error); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.unlinkForce = async function(filePath) { | ||||
| export async function unlinkForce(filePath: string) { | ||||
| 	const fs = require('fs-extra'); | ||||
| 
 | ||||
| 	try { | ||||
| @@ -212,13 +246,13 @@ toolUtils.unlinkForce = async function(filePath) { | ||||
| 		if (error.code === 'ENOENT') return; | ||||
| 		throw error; | ||||
| 	} | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.fileExists = async function(filePath) { | ||||
| export function fileExists(filePath: string) { | ||||
| 	const fs = require('fs-extra'); | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		fs.stat(filePath, function(err) { | ||||
| 		fs.stat(filePath, function(err: any) { | ||||
| 			if (err == null) { | ||||
| 				resolve(true); | ||||
| 			} else if (err.code == 'ENOENT') { | ||||
| @@ -228,27 +262,22 @@ toolUtils.fileExists = async function(filePath) { | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| async function loadGitHubUsernameCache() { | ||||
| 	const path = `${__dirname}/github_username_cache.json`; | ||||
| 
 | ||||
| 	if (await fs.exists(path)) { | ||||
| 		const jsonString = await fs.readFile(path); | ||||
| 		return JSON.parse(jsonString); | ||||
| 	} | ||||
| 
 | ||||
| 	return {}; | ||||
| } | ||||
| 
 | ||||
| async function saveGitHubUsernameCache(cache) { | ||||
| 	const path = `${__dirname}/github_username_cache.json`; | ||||
| 	await fs.writeFile(path, JSON.stringify(cache)); | ||||
| 
 | ||||
| export async function gitRepoClean(): Promise<boolean> { | ||||
| 	const output = await execCommand2('git status --porcelain', { quiet: true }); | ||||
| 	return !output.trim(); | ||||
| } | ||||
| 
 | ||||
| toolUtils.gitPullTry = async function() { | ||||
| 
 | ||||
| export async function gitRepoCleanTry() { | ||||
| 	if (!(await gitRepoClean())) throw new Error(`There are pending changes in the repository: ${process.cwd()}`); | ||||
| } | ||||
| 
 | ||||
| export async function gitPullTry() { | ||||
| 	try { | ||||
| 		await toolUtils.execCommand('git pull'); | ||||
| 		await execCommand('git pull'); | ||||
| 	} catch (error) { | ||||
| 		if (error.message.includes('no tracking information for the current branch')) { | ||||
| 			console.info('Skipping git pull because no tracking information on current branch'); | ||||
| @@ -256,16 +285,16 @@ toolUtils.gitPullTry = async function() { | ||||
| 			throw error; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.githubUsername = async function(email, name) { | ||||
| export async function githubUsername(email: string, name: string) { | ||||
| 	const cache = await loadGitHubUsernameCache(); | ||||
| 	const cacheKey = `${email}:${name}`; | ||||
| 	if (cacheKey in cache) return cache[cacheKey]; | ||||
| 
 | ||||
| 	let output = null; | ||||
| 
 | ||||
| 	const oauthToken = await toolUtils.githubOauthToken(); | ||||
| 	const oauthToken = await githubOauthToken(); | ||||
| 
 | ||||
| 	const urlsToTry = [ | ||||
| 		`https://api.github.com/search/users?q=${encodeURI(email)}+in:email`, | ||||
| @@ -296,23 +325,23 @@ toolUtils.githubUsername = async function(email, name) { | ||||
| 	await saveGitHubUsernameCache(cache); | ||||
| 
 | ||||
| 	return output; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.patreonOauthToken = async function() { | ||||
| 	return toolUtils.readCredentialFile('patreon_oauth_token.txt'); | ||||
| }; | ||||
| export function patreonOauthToken() { | ||||
| 	return readCredentialFile('patreon_oauth_token.txt'); | ||||
| } | ||||
| 
 | ||||
| toolUtils.githubOauthToken = async function() { | ||||
| 	return toolUtils.readCredentialFile('github_oauth_token.txt'); | ||||
| }; | ||||
| export function githubOauthToken() { | ||||
| 	return readCredentialFile('github_oauth_token.txt'); | ||||
| } | ||||
| 
 | ||||
| toolUtils.githubRelease = async function(project, tagName, options = null) { | ||||
| export async function githubRelease(project: string, tagName: string, options: any = null) { | ||||
| 	options = Object.assign({}, { | ||||
| 		isDraft: false, | ||||
| 		isPreRelease: false, | ||||
| 	}, options); | ||||
| 
 | ||||
| 	const oauthToken = await toolUtils.githubOauthToken(); | ||||
| 	const oauthToken = await githubOauthToken(); | ||||
| 
 | ||||
| 	const response = await fetch(`https://api.github.com/repos/laurent22/${project}/releases`, { | ||||
| 		method: 'POST', | ||||
| @@ -336,9 +365,9 @@ toolUtils.githubRelease = async function(project, tagName, options = null) { | ||||
| 	if (!responseJson.url) throw new Error(`No URL for release: ${responseText}`); | ||||
| 
 | ||||
| 	return responseJson; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.readline = question => { | ||||
| export function readline(question: string) { | ||||
| 	return new Promise((resolve) => { | ||||
| 		const readline = require('readline'); | ||||
| 
 | ||||
| @@ -347,63 +376,61 @@ toolUtils.readline = question => { | ||||
| 			output: process.stdout, | ||||
| 		}); | ||||
| 
 | ||||
| 		rl.question(`${question} `, answer => { | ||||
| 		rl.question(`${question} `, (answer: string) => { | ||||
| 			resolve(answer); | ||||
| 			rl.close(); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.isLinux = () => { | ||||
| export function isLinux() { | ||||
| 	return process && process.platform === 'linux'; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.isWindows = () => { | ||||
| export function isWindows() { | ||||
| 	return process && process.platform === 'win32'; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.isMac = () => { | ||||
| export function isMac() { | ||||
| 	return process && process.platform === 'darwin'; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.insertContentIntoFile = async function(filePath, markerOpen, markerClose, contentToInsert) { | ||||
| export async function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string) { | ||||
| 	const fs = require('fs-extra'); | ||||
| 	let content = await fs.readFile(filePath, 'utf-8'); | ||||
| 	// [^]* matches any character including new lines
 | ||||
| 	const regex = new RegExp(`${markerOpen}[^]*?${markerClose}`); | ||||
| 	content = content.replace(regex, markerOpen + contentToInsert + markerClose); | ||||
| 	await fs.writeFile(filePath, content); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.dirname = (path) => { | ||||
| export function dirname(path: string) { | ||||
| 	if (!path) throw new Error('Path is empty'); | ||||
| 	const s = path.split(/\/|\\/); | ||||
| 	s.pop(); | ||||
| 	return s.join('/'); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.basename = (path) => { | ||||
| export function basename(path: string) { | ||||
| 	if (!path) throw new Error('Path is empty'); | ||||
| 	const s = path.split(/\/|\\/); | ||||
| 	return s[s.length - 1]; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.filename = (path, includeDir = false) => { | ||||
| export function filename(path: string, includeDir = false) { | ||||
| 	if (!path) throw new Error('Path is empty'); | ||||
| 	const output = includeDir ? path : toolUtils.basename(path); | ||||
| 	const output = includeDir ? path : basename(path); | ||||
| 	if (output.indexOf('.') < 0) return output; | ||||
| 
 | ||||
| 	const splitted = output.split('.'); | ||||
| 	splitted.pop(); | ||||
| 	return splitted.join('.'); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| toolUtils.fileExtension = (path) => { | ||||
| export function fileExtension(path: string) { | ||||
| 	if (!path) throw new Error('Path is empty'); | ||||
| 
 | ||||
| 	const output = path.split('.'); | ||||
| 	if (output.length <= 1) return ''; | ||||
| 	return output[output.length - 1]; | ||||
| }; | ||||
| 
 | ||||
| module.exports = toolUtils; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user