1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Clipper: Improve support for future versions of Chrome (upgrade to manifest version 3) (#10109)

This commit is contained in:
Henry Heino 2024-03-14 11:38:20 -07:00 committed by GitHub
parent 78b8839ae3
commit e72cce0d07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 127 additions and 190 deletions

View File

@ -1229,6 +1229,7 @@ packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js packages/tools/release-android.js
packages/tools/release-cli.js packages/tools/release-cli.js
packages/tools/release-clipper.js
packages/tools/release-electron.js packages/tools/release-electron.js
packages/tools/release-ios.js packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js packages/tools/release-plugin-repo-cli.js

1
.gitignore vendored
View File

@ -1209,6 +1209,7 @@ packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js packages/tools/release-android.js
packages/tools/release-cli.js packages/tools/release-cli.js
packages/tools/release-clipper.js
packages/tools/release-electron.js packages/tools/release-electron.js
packages/tools/release-ios.js packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js packages/tools/release-plugin-repo-cli.js

View File

@ -11,13 +11,9 @@
if (typeof browser !== 'undefined') { if (typeof browser !== 'undefined') {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
browser_ = browser; browser_ = browser;
// eslint-disable-next-line no-undef
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') { } else if (typeof chrome !== 'undefined') {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
browser_ = chrome; browser_ = chrome;
// eslint-disable-next-line no-undef
browserSupportsPromises_ = false;
} }
function escapeHtml(s) { function escapeHtml(s) {
@ -461,6 +457,7 @@
tags: command.tags, tags: command.tags,
windowInnerWidth: window.innerWidth, windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight, windowInnerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
}; };
browser_.runtime.sendMessage({ browser_.runtime.sendMessage({

View File

@ -1,10 +1,12 @@
{ {
"manifest_version": 2, "manifest_version": 3,
"name": "Joplin Web Clipper [DEV]", "name": "Joplin Web Clipper [DEV]",
"version": "3.0.0", "version": "3.0.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.", "description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org", "homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'", "content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"icons": { "icons": {
"32": "icons/32.png", "32": "icons/32.png",
"48": "icons/48.png", "48": "icons/48.png",
@ -13,18 +15,21 @@
"permissions": [ "permissions": [
"activeTab", "activeTab",
"tabs", "tabs",
"http://*/", "scripting",
"https://*/",
"<all_urls>",
"storage" "storage"
], ],
"browser_action": { "host_permissions": [
"http://*/",
"https://*/",
"<all_urls>"
],
"action": {
"default_icon": "icons/32.png", "default_icon": "icons/32.png",
"default_title": "Joplin Web Clipper", "default_title": "Joplin Web Clipper",
"default_popup": "popup/build/index.html" "default_popup": "popup/build/index.html"
}, },
"commands": { "commands": {
"_execute_browser_action": { "_execute_action": {
"suggested_key": { "suggested_key": {
"default": "Alt+Shift+J" "default": "Alt+Shift+J"
} }
@ -49,12 +54,12 @@
} }
}, },
"background": { "background": {
"scripts": [ "scripts": ["service_worker.mjs"],
"background.js"
], "service_worker": "service_worker.mjs",
"persistent": false "type": "module"
}, },
"applications": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "{8419486a-54e9-11e8-9401-ac9e17909436}" "id": "{8419486a-54e9-11e8-9401-ac9e17909436}"
} }

View File

@ -115,6 +115,13 @@ class AppComponent extends Component {
this.clipScreenshot_click = async () => { this.clipScreenshot_click = async () => {
try { try {
// Firefox requires the <all_urls> host permission to take a
// screenshot of the current page, however, this may change
// in the future. Note that Firefox also forces this permission
// to be optional.
// See https://discourse.mozilla.org/t/browser-tabs-capturevisibletab-not-working-in-firefox-for-mv3/122965/3
await bridge().browser().permissions.request({ origins: ['<all_urls>'] });
const baseUrl = await bridge().clipperServerBaseUrl(); const baseUrl = await bridge().clipperServerBaseUrl();
await bridge().sendCommandToActiveTab({ await bridge().sendCommandToActiveTab({
@ -179,12 +186,14 @@ class AppComponent extends Component {
} }
async loadContentScripts() { async loadContentScripts() {
await bridge().tabsExecuteScript({ file: '/content_scripts/setUpEnvironment.js' }); await bridge().tabsExecuteScript([
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' }); '/content_scripts/setUpEnvironment.js',
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' }); '/content_scripts/JSDOMParser.js',
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' }); '/content_scripts/Readability.js',
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' }); '/content_scripts/Readability-readerable.js',
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' }); '/content_scripts/clipperUtils.js',
'/content_scripts/index.js',
]);
} }
async componentDidMount() { async componentDidMount() {

View File

@ -1,5 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import getActiveTabs from '../../util/getActiveTabs.mjs';
import joplinEnv from '../../util/joplinEnv.mjs';
const { randomClipperPort } = require('./randomClipperPort'); const { randomClipperPort } = require('./randomClipperPort');
function msleep(ms) { function msleep(ms) {
@ -17,13 +19,12 @@ class Bridge {
this.token_ = null; this.token_ = null;
} }
async init(browser, browserSupportsPromises, store) { async init(browser, store) {
console.info('Popup: Init bridge'); console.info('Popup: Init bridge');
this.browser_ = browser; this.browser_ = browser;
this.dispatch_ = store.dispatch; this.dispatch_ = store.dispatch;
this.store_ = store; this.store_ = store;
this.browserSupportsPromises_ = browserSupportsPromises;
this.clipperServerPort_ = null; this.clipperServerPort_ = null;
this.clipperServerPortStatus_ = 'searching'; this.clipperServerPortStatus_ = 'searching';
@ -74,12 +75,7 @@ class Bridge {
} }
}; };
this.browser_.runtime.onMessage.addListener(this.browser_notify); this.browser_.runtime.onMessage.addListener(this.browser_notify);
const backgroundPage = await this.backgroundPage(this.browser_); this.env_ = joplinEnv();
// Not sure why the getBackgroundPage() sometimes returns null, so
// in that case default to "prod" environment, which means the live
// extension won't be affected by this bug.
this.env_ = backgroundPage ? backgroundPage.joplinEnv() : 'prod';
console.info('Popup: Env:', this.env()); console.info('Popup: Env:', this.env());
@ -197,17 +193,6 @@ class Bridge {
} }
} }
async backgroundPage(browser) {
const bgp = browser.extension.getBackgroundPage();
if (bgp) return bgp;
return new Promise((resolve) => {
browser.runtime.getBackgroundPage((bgp) => {
resolve(bgp);
});
});
}
env() { env() {
return this.env_; return this.env_;
} }
@ -305,50 +290,26 @@ class Bridge {
return `http://127.0.0.1:${port}`; return `http://127.0.0.1:${port}`;
} }
async tabsExecuteScript(options) { async tabsExecuteScript(files) {
if (this.browserSupportsPromises_) return this.browser().tabs.executeScript(options); const activeTabs = await getActiveTabs(this.browser());
await this.browser().scripting.executeScript({
return new Promise((resolve, reject) => { target: {
this.browser().tabs.executeScript(options, () => { tabId: activeTabs[0].id,
const e = this.browser().runtime.lastError; },
if (e) { files,
const msg = [`tabsExecuteScript: Cannot load ${JSON.stringify(options)}`];
if (e.message) msg.push(e.message);
reject(new Error(msg.join(': ')));
}
resolve();
});
}); });
} }
async tabsQuery(options) { async tabsQuery(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.query(options); return this.browser().tabs.query(options);
return new Promise((resolve) => {
this.browser().tabs.query(options, (tabs) => {
resolve(tabs);
});
});
} }
async tabsSendMessage(tabId, command) { async tabsSendMessage(tabId, command) {
if (this.browserSupportsPromises_) return this.browser().tabs.sendMessage(tabId, command); return this.browser().tabs.sendMessage(tabId, command);
return new Promise((resolve) => {
this.browser().tabs.sendMessage(tabId, command, (result) => {
resolve(result);
});
});
} }
async tabsCreate(options) { async tabsCreate(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.create(options); return this.browser().tabs.create(options);
return new Promise((resolve) => {
this.browser().tabs.create(options, () => {
resolve();
});
});
} }
async folderTree() { async folderTree() {
@ -356,30 +317,16 @@ class Bridge {
} }
async storageSet(keys) { async storageSet(keys) {
if (this.browserSupportsPromises_) return this.browser().storage.local.set(keys); return this.browser().storage.local.set(keys);
return new Promise((resolve) => {
this.browser().storage.local.set(keys, () => {
resolve();
});
});
} }
async storageGet(keys, defaultValue = null) { async storageGet(keys, defaultValue = null) {
if (this.browserSupportsPromises_) {
try { try {
const r = await this.browser().storage.local.get(keys); const r = await this.browser().storage.local.get(keys);
return r; return r;
} catch (error) { } catch (error) {
return defaultValue; return defaultValue;
} }
} else {
return new Promise((resolve) => {
this.browser().storage.local.get(keys, (result) => {
resolve(result);
});
});
}
} }
async sendCommandToActiveTab(command) { async sendCommandToActiveTab(command) {

View File

@ -112,7 +112,7 @@ async function main() {
console.info('Popup: Init bridge and restore state...'); console.info('Popup: Init bridge and restore state...');
await bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store); await bridge().init(window.browser ? window.browser : window.chrome, store);
console.info('Popup: Creating React app...'); console.info('Popup: Creating React app...');

View File

@ -1,25 +1,13 @@
import joplinEnv from './util/joplinEnv.mjs';
import getActiveTabs from './util/getActiveTabs.mjs';
let browser_ = null; let browser_ = null;
if (typeof browser !== 'undefined') { if (typeof browser !== 'undefined') {
browser_ = browser; browser_ = browser;
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') { } else if (typeof chrome !== 'undefined') {
browser_ = chrome; browser_ = chrome;
browserSupportsPromises_ = false;
} }
let env_ = null;
// Make this function global so that it can be accessed
// from the popup too.
// https://stackoverflow.com/questions/6323184/communication-between-background-page-and-popup-page-in-a-chrome-extension
window.joplinEnv = function() {
if (env_) return env_;
const manifest = browser_.runtime.getManifest();
env_ = manifest.name.indexOf('[DEV]') >= 0 ? 'dev' : 'prod';
return env_;
};
async function browserCaptureVisibleTabs(windowId) { async function browserCaptureVisibleTabs(windowId) {
const options = { const options = {
format: 'jpeg', format: 'jpeg',
@ -31,53 +19,19 @@ async function browserCaptureVisibleTabs(windowId) {
// https://discourse.joplinapp.org/t/clip-screenshot-image-quality/12302/4 // https://discourse.joplinapp.org/t/clip-screenshot-image-quality/12302/4
quality: 92, quality: 92,
}; };
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, options); return browser_.tabs.captureVisibleTab(windowId, options);
return new Promise((resolve) => {
browser_.tabs.captureVisibleTab(windowId, options, (image) => {
resolve(image);
});
});
}
async function browserGetZoom(tabId) {
if (browserSupportsPromises_) return browser_.tabs.getZoom(tabId);
return new Promise((resolve) => {
browser_.tabs.getZoom(tabId, (zoom) => {
resolve(zoom);
});
});
} }
browser_.runtime.onInstalled.addListener(() => { browser_.runtime.onInstalled.addListener(() => {
if (window.joplinEnv() === 'dev') { if (joplinEnv() === 'dev') {
browser_.browserAction.setIcon({ browser_.action.setIcon({
path: 'icons/32-dev.png', path: 'icons/32-dev.png',
}); });
} }
}); });
async function getImageSize(dataUrl) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({ width: image.width, height: image.height });
};
image.onerror = function(event) {
reject(event);
};
image.src = dataUrl;
});
}
browser_.runtime.onMessage.addListener(async (command) => { browser_.runtime.onMessage.addListener(async (command) => {
if (command.name === 'screenshotArea') { if (command.name === 'screenshotArea') {
const browserZoom = await browserGetZoom();
// The dimensions of the image returned by Firefox are the regular ones, // The dimensions of the image returned by Firefox are the regular ones,
// while the one returned by Chrome depend on the screen pixel ratio. So // while the one returned by Chrome depend on the screen pixel ratio. So
// it would return a 600*400 image if the window dimensions are 300x200 // it would return a 600*400 image if the window dimensions are 300x200
@ -90,15 +44,18 @@ browser_.runtime.onMessage.addListener(async (command) => {
// //
// The crop rectangle is always in real pixels, so we need to multiply // The crop rectangle is always in real pixels, so we need to multiply
// it by the ratio we've calculated. // it by the ratio we've calculated.
//
// 8/3/2024: With manifest v3, we don't have access to DOM APIs in Chrome.
// As a result, we can't easily calculate the size of the captured image.
// We instead base the crop region exclusively on window.devicePixelRatio,
// which seems to work in modern Firefox and Chrome.
const imageDataUrl = await browserCaptureVisibleTabs(null); const imageDataUrl = await browserCaptureVisibleTabs(null);
const imageSize = await getImageSize(imageDataUrl);
const imagePixelRatio = imageSize.width / command.content.windowInnerWidth;
const content = { ...command.content }; const content = { ...command.content };
content.image_data_url = imageDataUrl; content.image_data_url = imageDataUrl;
if ('url' in content) content.source_url = content.url; if ('url' in content) content.source_url = content.url;
const ratio = browserZoom * imagePixelRatio; const ratio = content.devicePixelRatio;
const newArea = { ...command.content.crop_rect }; const newArea = { ...command.content.crop_rect };
newArea.x *= ratio; newArea.x *= ratio;
newArea.y *= ratio; newArea.y *= ratio;
@ -117,19 +74,8 @@ browser_.runtime.onMessage.addListener(async (command) => {
} }
}); });
async function getActiveTabs() {
const options = { active: true, currentWindow: true };
if (browserSupportsPromises_) return browser_.tabs.query(options);
return new Promise((resolve) => {
browser_.tabs.query(options, (tabs) => {
resolve(tabs);
});
});
}
async function sendClipMessage(clipType) { async function sendClipMessage(clipType) {
const tabs = await getActiveTabs(); const tabs = await getActiveTabs(browser_);
if (!tabs || !tabs.length) { if (!tabs || !tabs.length) {
console.error('No active tabs'); console.error('No active tabs');
return; return;

View File

@ -0,0 +1,7 @@
const getActiveTabs = async (browser) => {
const options = { active: true, currentWindow: true };
return await browser.tabs.query(options);
}
export default getActiveTabs;

View File

@ -0,0 +1,3 @@
// AUTOGENERATED by release-clipper
export default () => 'dev';

View File

@ -1,26 +1,27 @@
const fs = require('fs-extra'); import * as fs from 'fs-extra';
const { execCommand, rootDir } = require('./tool-utils.js'); import { execCommand, rootDir } from './tool-utils';
const md5File = require('md5-file'); const md5File = require('md5-file');
const glob = require('glob'); import * as glob from 'glob';
const clipperDir = `${rootDir}/packages/app-clipper`; const clipperDir = `${rootDir}/packages/app-clipper`;
const tmpSourceDirName = 'Clipper-source'; const tmpSourceDirName = 'Clipper-source';
async function copyDir(baseSourceDir, sourcePath, baseDestDir) { async function copyDir(baseSourceDir: string, sourcePath: string, baseDestDir: string) {
await fs.mkdirp(`${baseDestDir}/${sourcePath}`); await fs.mkdirp(`${baseDestDir}/${sourcePath}`);
await fs.copy(`${baseSourceDir}/${sourcePath}`, `${baseDestDir}/${sourcePath}`); await fs.copy(`${baseSourceDir}/${sourcePath}`, `${baseDestDir}/${sourcePath}`);
} }
async function copyToDist(distDir) { async function copyToDist(distDir: string) {
await copyDir(clipperDir, 'popup/build', distDir); await copyDir(clipperDir, 'popup/build', distDir);
await copyDir(clipperDir, 'content_scripts', distDir); await copyDir(clipperDir, 'content_scripts', distDir);
await copyDir(clipperDir, 'icons', distDir); await copyDir(clipperDir, 'icons', distDir);
await fs.copy(`${clipperDir}/background.js`, `${distDir}/background.js`); await copyDir(clipperDir, 'util', distDir);
await fs.copy(`${clipperDir}/service_worker.mjs`, `${distDir}/service_worker.mjs`);
await fs.copy(`${clipperDir}/manifest.json`, `${distDir}/manifest.json`); await fs.copy(`${clipperDir}/manifest.json`, `${distDir}/manifest.json`);
await fs.remove(`${distDir}/popup/build/manifest.json`); await fs.remove(`${distDir}/popup/build/manifest.json`);
} }
async function updateManifestVersionNumber(manifestPath) { async function updateManifestVersionNumber(manifestPath: string) {
const manifestText = await fs.readFile(manifestPath, 'utf-8'); const manifestText = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestText); const manifest = JSON.parse(manifestText);
const v = manifest.version.split('.'); const v = manifest.version.split('.');
@ -47,11 +48,11 @@ async function createSourceZip() {
return filePath; return filePath;
} }
async function compareFiles(path1, path2) { async function compareFiles(path1: string, path2: string) {
return await md5File(path1) === await md5File(path2); return await md5File(path1) === await md5File(path2);
} }
async function compareDir(dir1, dir2) { async function compareDir(dir1: string, dir2: string) {
console.info(`Comparing directories ${dir1} to ${dir2}`); console.info(`Comparing directories ${dir1} to ${dir2}`);
const globOptions = { const globOptions = {
@ -61,7 +62,7 @@ async function compareDir(dir1, dir2) {
], ],
}; };
const filterFiles = (f) => { const filterFiles = (f: string) => {
const stat = fs.statSync(f); const stat = fs.statSync(f);
return !stat.isDirectory(); return !stat.isDirectory();
}; };
@ -69,11 +70,11 @@ async function compareDir(dir1, dir2) {
const files1 = glob.sync(`${dir1}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir1.length + 1)); const files1 = glob.sync(`${dir1}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir1.length + 1));
const files2 = glob.sync(`${dir2}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir2.length + 1)); const files2 = glob.sync(`${dir2}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir2.length + 1));
const missingFiles1 = []; const missingFiles1: string[] = [];
const missingFiles2 = []; const missingFiles2: string[] = [];
const canBeMissing1 = []; const canBeMissing1: string[] = [];
const canBeMissing2 = ['manifest.json']; const canBeMissing2: string[] = ['manifest.json'];
const differentFiles = []; const differentFiles: string[] = [];
for (const f of files1) { for (const f of files1) {
if (!files2.includes(f)) { if (!files2.includes(f)) {
if (canBeMissing2.includes(f)) continue; if (canBeMissing2.includes(f)) continue;
@ -104,7 +105,7 @@ async function compareDir(dir1, dir2) {
return false; return false;
} }
async function checkSourceZip(sourceZip, compiledZip) { async function checkSourceZip(sourceZip: string, compiledZip: string) {
const tmpDir = `${require('os').tmpdir()}/${Date.now()}`; const tmpDir = `${require('os').tmpdir()}/${Date.now()}`;
console.info(`Checking source ZIP in ${tmpDir}`); console.info(`Checking source ZIP in ${tmpDir}`);
@ -119,6 +120,7 @@ async function checkSourceZip(sourceZip, compiledZip) {
console.info(await execCommand(`unzip "${sourceZip}"`)); console.info(await execCommand(`unzip "${sourceZip}"`));
process.chdir(`${sourceDir}/Clipper-source/popup`); process.chdir(`${sourceDir}/Clipper-source/popup`);
console.info(await execCommand('npm install')); console.info(await execCommand('npm install'));
console.info(await execCommand('npm run build'));
process.chdir(compiledDir); process.chdir(compiledDir);
console.info(await execCommand(`cp "${compiledZip}" .`)); console.info(await execCommand(`cp "${compiledZip}" .`));
@ -132,6 +134,11 @@ async function checkSourceZip(sourceZip, compiledZip) {
} }
} }
async function setReleaseMode(isReleaseMode: boolean) {
const joplinEnvPath = `${clipperDir}/util/joplinEnv.mjs`;
await fs.writeFile(joplinEnvPath, `// AUTOGENERATED by release-clipper\n\nexport default () => '${isReleaseMode ? 'prod' : 'dev'}';`);
}
async function main() { async function main() {
console.info(await execCommand('git pull')); console.info(await execCommand('git pull'));
@ -139,24 +146,34 @@ async function main() {
console.info('Building extension...'); console.info('Building extension...');
process.chdir(`${clipperDir}/popup`); process.chdir(`${clipperDir}/popup`);
// SKIP_PREFLIGHT_CHECK avoids the error "There might be a problem with the project dependency tree." due to eslint 5.12.0 being await setReleaseMode(true);
// installed by CRA and 6.1.0 by us. It doesn't affect anything though, and the behaviour of the preflight
// check is buggy so we can ignore it.
console.info(await execCommand(`rm -rf ${clipperDir}/popup/build`)); console.info(await execCommand(`rm -rf ${clipperDir}/popup/build`));
console.info(await execCommand('npm run build')); console.info(await execCommand('npm run build'));
const dists = { type PlatformDistOptions = {
removeManifestKeys(manifest: Record<string, any>): Record<string, any>;
outputPath?: string;
};
const dists: Record<string, PlatformDistOptions> = {
chrome: { chrome: {
removeManifestKeys: (manifest) => { removeManifestKeys: (manifest) => {
manifest = { ...manifest }; manifest = { ...manifest };
delete manifest.applications; delete manifest.browser_specific_settings;
manifest.background = { ...manifest.background };
delete manifest.background.scripts;
delete manifest.background.persistent;
return manifest; return manifest;
}, },
}, },
firefox: { firefox: {
removeManifestKeys: (manifest) => { removeManifestKeys: (manifest) => {
manifest = { ...manifest }; manifest = { ...manifest };
delete manifest.background.persistent;
manifest.background = { ...manifest.background };
delete manifest.background.service_worker;
return manifest; return manifest;
}, },
}, },
@ -186,13 +203,17 @@ async function main() {
const sourceZip = await createSourceZip(); const sourceZip = await createSourceZip();
await checkSourceZip(sourceZip, dists.firefox.outputPath); await checkSourceZip(sourceZip, dists.firefox.outputPath);
await setReleaseMode(false);
process.chdir(clipperDir); process.chdir(clipperDir);
if (!process.argv.includes('--no-publish')) {
console.info(await execCommand('git add -A')); console.info(await execCommand('git add -A'));
console.info(await execCommand(`git commit -m "Clipper release v${newVersion}"`)); console.info(await execCommand(`git commit -m "Clipper release v${newVersion}"`));
console.info(await execCommand(`git tag clipper-${newVersion}`)); console.info(await execCommand(`git tag clipper-${newVersion}`));
console.info(await execCommand('git push')); console.info(await execCommand('git push'));
console.info(await execCommand('git push --tags')); console.info(await execCommand('git push --tags'));
} }
}
main().catch((error) => { main().catch((error) => {
console.error('Fatal error'); console.error('Fatal error');