Compare commits

..
Author SHA1 Message Date
Laurent Cozic eddfdfa9db Tools: Queue concurrent sub-package publish runs instead of cancelling them
Ref #16659
2026-11-11 18:55:47 +00:00
Laurent Cozic 26db2b046a Tools: Address review comments on sub-package publishing
- Use the npm bundled with Node 24.18 rather than installing an unpinned version
- Pass registry-url through the shared build action instead of setting up Node twice
- Prevent concurrent publish runs
- Restore the working directory after publishing each package
- Compile all workspaces before versioning, since tools uses the compiled @joplin/utils

Ref #16659
2026-11-11 14:02:15 +00:00
Laurent Cozic 9e3a2af71c Tools: Bundle plugin-repo-cli before publishing sub-packages
Ref #16659
2026-11-11 13:51:06 +00:00
Laurent Cozic cd7e7144e1 Tools: Publish sub-packages from CI using npm trusted publishing
`yarn publishAll` now bumps the versions, commits and pushes a sub-packages-v* tag, which triggers the publish-sub-packages workflow.

Ref #16659
2026-11-11 12:10:16 +00:00
Laurent Cozic 33a1af323d Tools: Set repository field of published packages to the Joplin repository
Required for npm provenance.

Ref #16659
2026-11-11 12:08:09 +00:00
Laurent Cozic 6cc769c0aa Tools: Replace lerna with a custom script to version and publish sub-packages
Lerna 3 can no longer build its package graph because of the yarn patch: protocol, and it cannot use npm trusted publishing.

Ref #16659
2026-11-11 12:07:54 +00:00
41 changed files with 429 additions and 3738 deletions

No files matched your search

@@ -0,0 +1,47 @@
name: Publish sub-packages
# The tag is created and pushed by `yarn publishAll`. Publishing uses npm trusted publishing, so no
# token is needed, but each package must have this workflow registered as a trusted publisher on
# npmjs.com.
on:
push:
tags:
- 'sub-packages-v*'
permissions:
contents: read
id-token: write
# Two concurrent runs could both see a version as unpublished and then fail on the second publish
concurrency:
group: publish-sub-packages
cancel-in-progress: false
queue: max
jobs:
Publish:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
# Trusted publishing requires registry-url, and npm >= 11.5.1, which is bundled with Node 24.18
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
with:
registry-url: 'https://registry.npmjs.org'
- name: Install and build
env:
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
# Not part of the regular build, but plugin-repo-cli's entry point is dist/index.js
- name: Bundle plugin-repo-cli
run: yarn workspace @joplin/plugin-repo-cli dist
- name: Publish
run: node packages/tools/releaseSubPackages.js publish
@@ -1,5 +1,10 @@
name: 'Setup build environment'
description: 'Install Joplin build dependencies'
inputs:
registry-url:
description: 'npm registry URL, required for npm trusted publishing'
required: false
default: ''
runs:
using: 'composite'
steps:
@@ -55,6 +60,7 @@ runs:
- uses: actions/setup-node@v6
with:
node-version: '24.18'
registry-url: ${{ inputs.registry-url }}
# Disable the cache on ARM runners. For now, we don't run "yarn install" on these
# environments and this breaks actions/setup-node.
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
+2 -4
View File
@@ -686,7 +686,6 @@ packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
packages/app-desktop/services/plugins/hooks/useThemeCss.js
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/startBlockedPluginsCheck.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
@@ -1882,11 +1881,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
packages/lib/services/plugins/loadPlugins.test.js
packages/lib/services/plugins/loadPlugins.js
packages/lib/services/plugins/reducer.js
packages/lib/services/plugins/refreshBlockedPluginVersions.js
packages/lib/services/plugins/testing/MockPlatformImplementation.js
packages/lib/services/plugins/testing/MockPluginRunner.js
packages/lib/services/plugins/utils/blockedVersions.test.js
packages/lib/services/plugins/utils/blockedVersions.js
packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getActivePluginEditorView.js
@@ -2262,6 +2258,8 @@ packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js
packages/tools/release-server.js
packages/tools/release-transcribe.js
packages/tools/releaseSubPackages.test.js
packages/tools/releaseSubPackages.js
packages/tools/saveClaConsentRecords.js
packages/tools/setupNewRelease.js
packages/tools/spellcheck.js
+2 -4
View File
@@ -712,7 +712,6 @@ packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
packages/app-desktop/services/plugins/hooks/useThemeCss.js
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/startBlockedPluginsCheck.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
@@ -1908,11 +1907,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
packages/lib/services/plugins/loadPlugins.test.js
packages/lib/services/plugins/loadPlugins.js
packages/lib/services/plugins/reducer.js
packages/lib/services/plugins/refreshBlockedPluginVersions.js
packages/lib/services/plugins/testing/MockPlatformImplementation.js
packages/lib/services/plugins/testing/MockPluginRunner.js
packages/lib/services/plugins/utils/blockedVersions.test.js
packages/lib/services/plugins/utils/blockedVersions.js
packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getActivePluginEditorView.js
@@ -2288,6 +2284,8 @@ packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js
packages/tools/release-server.js
packages/tools/release-transcribe.js
packages/tools/releaseSubPackages.test.js
packages/tools/releaseSubPackages.js
packages/tools/saveClaConsentRecords.js
packages/tools/setupNewRelease.js
packages/tools/spellcheck.js
+7 -12
View File
@@ -1,6 +1,7 @@
const gulp = require('gulp');
const execa = require('execa');
const { stdout } = require('process');
const { readFileSync } = require('fs');
const execCommand = async (executableName, args, options = null) => {
options = {
@@ -32,21 +33,15 @@ const execCommand = async (executableName, args, options = null) => {
const tasks = {
completePublishAll: {
fn: async () => {
await execCommand('yarn', ['install', '--mode=update-lockfile']);
await execCommand('git', ['add', '-A']);
await execCommand('git', ['commit', '-m', 'Releasing sub-packages']);
// Lerna does some unnecessary auth check that doesn't work with
// automation tokens, thus the --no-verify-access. Automation token
// is still used for access when publishing even with this flag
// (publishing would fail otherwise).
// https://github.com/lerna/lerna/issues/2788
await execCommand('lerna', ['publish', 'from-package', '-y', '--no-verify-access']);
await execCommand('yarn', ['install']);
await execCommand('git', ['add', '-A']);
await execCommand('git', ['commit', '-m', 'Lock file']);
await execCommand('git', ['push']);
// Pushing the tag triggers the publish-sub-packages workflow, which publishes the packages
const libVersion = JSON.parse(readFileSync('packages/lib/package.json', 'utf8')).version;
const tagName = `sub-packages-v${libVersion}`;
await execCommand('git', ['tag', tagName]);
await execCommand('git', ['push', '--atomic', 'origin', 'HEAD', tagName]);
},
},
build: {
-7
View File
@@ -1,7 +0,0 @@
{
"packages": [
"packages/*"
],
"version": "independent",
"useWorkspaces": true
}
+1 -2
View File
@@ -42,7 +42,7 @@
"syncFuzzer": "node ./packages/tools/fuzzer/cli.js",
"postinstall": "husky && gulp build",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"publishAll": "git pull && yarn tsc && node packages/tools/releaseSubPackages.js version && gulp completePublishAll",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@17/bin:$PATH\" node packages/tools/release-android.js",
"releaseAndroidClean": "node packages/tools/release-android.js",
"releaseCli": "node packages/tools/release-cli.js",
@@ -92,7 +92,6 @@
"globals": "17.7.0",
"gulp": "5.0.1",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "16.4.0",
"madge": "8.0.0",
"npm-package-json-lint": "10.4.1",
@@ -1,26 +1,14 @@
import { AppType } from '@joplin/lib/models/Setting';
import RepositoryApi, { AppInfo, InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import refreshBlockedPluginVersions from '@joplin/lib/services/plugins/refreshBlockedPluginVersions';
import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir } from '@joplin/lib/testing/test-utils';
import { copy, mkdirp, readJson, remove, writeFile, writeJson } from 'fs-extra';
import { remove } from 'fs-extra';
let tempDirs: string[] = [];
async function newRepoApi(appInfo: AppInfo = { type: AppType.Desktop, version: '3.0.0' }, blockedVersions: Record<string, string[]> = null): Promise<RepositoryApi> {
async function newRepoApi(appInfo: AppInfo = { type: AppType.Desktop, version: '3.0.0' }): Promise<RepositoryApi> {
const tempDir = await createTempDir();
tempDirs.push(tempDir);
let repoDir = `${supportDir}/pluginRepo`;
if (blockedVersions) {
repoDir = `${tempDir}/pluginRepo`;
await copy(`${supportDir}/pluginRepo`, repoDir);
const manifests = await readJson(`${repoDir}/manifests.json`);
for (const [id, versions] of Object.entries(blockedVersions)) manifests[id]._blocked_versions = versions;
await writeJson(`${repoDir}/manifests.json`, manifests);
}
const repo = new RepositoryApi(repoDir, tempDir, appInfo, InstallMode.Default);
const repo = new RepositoryApi(`${supportDir}/pluginRepo`, tempDir, appInfo, InstallMode.Default);
await repo.initialize();
return repo;
}
@@ -79,33 +67,4 @@ describe('services_plugins_RepositoryApi', () => {
expect(await api.pluginCanBeUpdated(id, installedVersion)).toBe(shouldBeUpdatable);
}));
it.each([
['1.0.2'],
['>=1.0.1'],
['*'],
])('should not update to or download a blocked version (%s)', (async (blockedVersion) => {
const id = 'org.joplinapp.plugins.ToggleSidebars';
const api = await newRepoApi(undefined, { [id]: [blockedVersion] });
expect(await api.pluginCanBeUpdated(id, '1.0.0')).toBe(false);
await expect(api.downloadPlugin(id)).rejects.toThrow('has been blocked');
}));
it('should cache the blocked versions only when plugins are installed', (async () => {
const blockedVersions = { 'org.joplinapp.plugins.ToggleSidebars': ['1.0.1', '1.0.2'] };
const api = await newRepoApi(undefined, blockedVersions);
await refreshBlockedPluginVersions(api);
expect(Setting.value('plugins.blockedVersions')).toEqual({});
const pluginPath = `${Setting.value('pluginDir')}/test.jpl`;
await mkdirp(Setting.value('pluginDir'));
await writeFile(pluginPath, '');
try {
await refreshBlockedPluginVersions(api);
expect(Setting.value('plugins.blockedVersions')).toEqual(blockedVersions);
} finally {
await remove(pluginPath);
}
}));
});
-3
View File
@@ -18,7 +18,6 @@ import { _, setLocale } from '@joplin/lib/locale';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative';
import bridge from './services/bridge';
import startBlockedPluginsCheck from './services/plugins/startBlockedPluginsCheck';
import menuCommandNames from './gui/menuCommandNames';
import ResourceService from '@joplin/lib/services/ResourceService';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
@@ -386,8 +385,6 @@ class Application extends BaseApplication {
ipcRenderer.send('startup-plugins-loaded');
void afterDefaultPluginsLoaded(service.plugins, getDefaultPluginsInfo(), pluginSettings);
startBlockedPluginsCheck(this.store());
}
}, 500);
}
@@ -1,60 +0,0 @@
import Setting, { AppType } from '@joplin/lib/models/Setting';
import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import refreshBlockedPluginVersions from '@joplin/lib/services/plugins/refreshBlockedPluginVersions';
import { State, stateUtils } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import time from '@joplin/lib/time';
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/utils/Logger';
import { Store } from 'redux';
import bridge from '../bridge';
import restart from '../restart';
const logger = Logger.create('startBlockedPluginsCheck');
const waitForNotesSaved = async (store: Store<State>) => {
const startTime = Date.now();
while (stateUtils.hasNotesBeingSaved(store.getState()) && Date.now() - startTime < 5000) {
await time.msleep(100);
}
};
// A blocked plugin can't be fully unloaded on desktop since its content scripts run in the main
// window, so we restart straight away rather than wait for the user, who may not be at their computer.
const restartForBlockedPlugins = async (store: Store<State>, pluginIds: string[]) => {
logger.warn('Restarting because these running plugins have been blocked:', pluginIds);
const service = PluginService.instance();
Setting.setValue('plugins.blockedRestartNotice', pluginIds.map(id => service.pluginById(id).manifest.name));
await waitForNotesSaved(store);
await restart();
};
const showBlockedRestartNotice = () => {
const pluginNames: string[] = Setting.value('plugins.blockedRestartNotice');
if (!pluginNames.length) return;
Setting.setValue('plugins.blockedRestartNotice', []);
bridge().showInfoMessageBox(_('Joplin was restarted because the following plugins have been disabled by the Joplin team for security reasons: %s\n\nWe recommend uninstalling them.', pluginNames.join(', ')));
};
const startBlockedPluginsCheck = (store: Store<State>) => {
showBlockedRestartNotice();
const repoApi = RepositoryApi.ofDefaultJoplinRepo(
Setting.value('tempDir'), { type: AppType.Desktop, version: PluginService.instance().appVersion }, InstallMode.Default,
);
const check = async () => {
try {
const blockedRunningPluginIds = await refreshBlockedPluginVersions(repoApi);
if (blockedRunningPluginIds.length) await restartForBlockedPlugins(store, blockedRunningPluginIds);
} catch (error) {
logger.warn('Could not refresh blocked plugin versions:', error);
}
};
void check();
shim.setInterval(() => { void check(); }, 60 * 60 * 1000);
};
export default startBlockedPluginsCheck;
@@ -9,7 +9,6 @@ import { connect, useStore } from 'react-redux';
import Logger from '@joplin/utils/Logger';
import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
import { BlockedPluginVersions } from '@joplin/lib/services/plugins/utils/blockedVersions';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import PluginDialogManager from './dialogs/PluginDialogManager';
import { AppState } from '../../utils/types';
@@ -34,7 +33,6 @@ const usePlugins = (
pluginSettings: PluginSettings,
pluginSupportEnabled: boolean,
devPluginPath: string,
blockedPluginVersions: BlockedPluginVersions,
) => {
const store = useStore<AppState>();
const lastPluginRunner = usePrevious(pluginRunner);
@@ -73,7 +71,7 @@ const usePlugins = (
if (!event.cancelled) {
reloadAllRef.current = false;
}
}, [pluginRunner, store, webviewLoaded, pluginSettings, reloadCounter, blockedPluginVersions]);
}, [pluginRunner, store, webviewLoaded, pluginSettings, reloadCounter]);
};
const useUnloadPluginsOnGlobalDisable = (
@@ -97,7 +95,6 @@ interface Props {
pluginSupportEnabled: boolean;
pluginStates: PluginStates;
devPluginPath: string;
blockedPluginVersions: BlockedPluginVersions;
pluginHtmlContents: PluginHtmlContents;
themeId: number;
}
@@ -128,7 +125,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
}, [webviewReloadCounter]);
const pluginSettings = usePluginSettings(props.serializedPluginSettings);
usePlugins(pluginRunner, webviewLoaded, pluginSettings, props.pluginSupportEnabled, props.devPluginPath, props.blockedPluginVersions);
usePlugins(pluginRunner, webviewLoaded, pluginSettings, props.pluginSupportEnabled, props.devPluginPath);
useUnloadPluginsOnGlobalDisable(props.pluginStates, props.pluginSupportEnabled);
const onLoadStart = useCallback(() => {
@@ -216,7 +213,6 @@ export default connect((state: AppState) => {
serializedPluginSettings: state.settings['plugins.states'],
pluginSupportEnabled: state.settings['plugins.pluginSupportEnabled'],
devPluginPath: state.settings['plugins.devPluginPaths'],
blockedPluginVersions: state.settings['plugins.blockedVersions'],
pluginStates: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
themeId: state.settings.theme,
@@ -8,8 +8,6 @@ import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';
import refreshBlockedPluginVersions from '@joplin/lib/services/plugins/refreshBlockedPluginVersions';
import { parseNotesParent } from '@joplin/lib/reducer';
import uuid from '@joplin/lib/uuid';
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
@@ -517,21 +515,6 @@ const buildStartupTasks = (
const updatedSettings = pluginService.clearUpdateState(pluginSettings);
Setting.setValue('plugins.states', updatedSettings);
// Running plugins that are now blocked are unloaded by PluginRunnerWebView when the setting changes
const repoApi = RepositoryApi.ofDefaultJoplinRepo(
Setting.value('tempDir'), { type: AppType.Mobile, version: platformImplementation.versionInfo.version }, InstallMode.Default,
);
const checkBlockedPlugins = async () => {
if (!Setting.value('plugins.pluginSupportEnabled')) return;
try {
await refreshBlockedPluginVersions(repoApi);
} catch (error) {
logger.warn('Could not refresh blocked plugin versions:', error);
}
};
void checkBlockedPlugins();
PoorManIntervals.setInterval(() => { void checkBlockedPlugins(); }, 60 * 60 * 1000);
});
addTask('buildStartupTasks/run startup tests', async () => {
// ----------------------------------------------------------------------------
+2 -1
View File
@@ -18,7 +18,8 @@
],
"repository": {
"type": "git",
"url": "git://github.com/fb55/htmlparser2.git"
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/fork-htmlparser2"
},
"directories": {
"lib": "lib/"
+5 -1
View File
@@ -8,7 +8,11 @@
"access": "public"
},
"license": "ISC",
"repository": "git://github.com/isaacs/sax-js.git",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/fork-sax"
},
"files": [
"lib/sax.js",
"LICENSE",
+2 -1
View File
@@ -23,7 +23,8 @@
},
"repository": {
"type": "git",
"url": "http://github.com/jeremys/uslug.git"
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/fork-uslug"
},
"main": "./index",
"exports": {
+5
View File
@@ -2,6 +2,11 @@
"name": "@joplin/htmlpack",
"version": "3.7.3",
"description": "Pack an HTML file and all its linked resources into a single HTML file",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/htmlpack"
},
"main": "dist/index.js",
"types": "index.ts",
"exports": {
@@ -7,7 +7,6 @@ import type SettingType from '../Setting';
import { AppType, SettingItemSubType, SettingItemType, SettingStorage, SyncStartupOperation, SettingItem, SettingButtonStyle } from './types';
import { defaultListColumns } from '../../services/plugins/api/noteListType';
import type { PluginSettings } from '../../services/plugins/PluginService';
import type { BlockedPluginVersions } from '../../services/plugins/utils/blockedVersions';
import type { PublicPrivateKeyPair } from '../../services/e2ee/ppk/ppk';
import type { NoteLockKeyConflict } from '../../services/synchronizer/syncInfoUtils';
import { EmptyObject } from '@joplin/utils/types';
@@ -1448,20 +1447,6 @@ const builtInMetadata = (Setting: typeof SettingType) => {
autoSave: true,
},
'plugins.blockedVersions': {
value: {} as BlockedPluginVersions,
type: SettingItemType.Object,
public: false,
appTypes: [AppType.Desktop, AppType.Mobile],
},
'plugins.blockedRestartNotice': {
value: [] as string[],
type: SettingItemType.Array,
public: false,
appTypes: [AppType.Desktop],
},
'plugins.enableWebviewDebugging': {
value: false,
type: SettingItemType.Bool,
+5
View File
@@ -2,6 +2,11 @@
"name": "@joplin/lib",
"version": "3.7.3",
"description": "Joplin Core library",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/lib"
},
"author": "Laurent Cozic",
"homepage": "",
"license": "AGPL-3.0-or-later",
+2 -12
View File
@@ -10,7 +10,6 @@ import Logger from '@joplin/utils/Logger';
import RepositoryApi from './RepositoryApi';
import { produce } from 'immer';
import { PluginManifest } from './utils/types';
import { BlockedPluginVersions, isRepoManifestBlocked, isVersionBlocked } from './utils/blockedVersions';
import isCompatible from './utils/isCompatible';
import { AppType } from './api/types';
import minVersionForPlatform from './utils/isCompatible/minVersionForPlatform';
@@ -512,9 +511,7 @@ export default class PluginService extends BaseService {
// Load only the manifest first to check if the plugin is
// enabled before doing the expensive full load.
let plugin = await this.loadPluginFromPath(pluginPath, true);
const blocked = this.isBlocked(plugin.manifest);
if (blocked) logger.warn(`Plugin version has been blocked: ${plugin.id}@${plugin.manifest.version}`);
const enabled = this.pluginEnabled(settings, plugin.id) && !blocked;
const enabled = this.pluginEnabled(settings, plugin.id);
if (enabled) {
logger.info(`Loading full plugin: ${plugin.id}`);
plugin = await this.loadPluginFromPath(pluginPath, false);
@@ -586,13 +583,8 @@ export default class PluginService extends BaseService {
return shim.mobilePlatform() ? AppType.Mobile : AppType.Desktop;
}
public isBlocked(manifest: PluginManifest) {
const blockedVersions: BlockedPluginVersions = Setting.value('plugins.blockedVersions');
return isRepoManifestBlocked(manifest) || isVersionBlocked(blockedVersions?.[manifest.id], manifest.version);
}
public isCompatible(manifest: PluginManifest): boolean {
return isCompatible(this.appVersion_, this.appType_, manifest) && !this.isBlocked(manifest);
return isCompatible(this.appVersion_, this.appType_, manifest);
}
private validateManifest(manifest: unknown): void {
@@ -608,8 +600,6 @@ export default class PluginService extends BaseService {
return _('Invalid plugin manifest: %s', message);
}
if (this.isBlocked(manifest)) return _('This version of the plugin has been disabled by the Joplin team for security reasons.');
if (this.isCompatible(manifest)) return null;
const minVersion = minVersionForPlatform(this.appType_, manifest);
+4 -11
View File
@@ -4,7 +4,6 @@ import { PluginManifest } from './utils/types';
const md5 = require('md5');
import { compareVersions } from 'compare-versions';
import isCompatible from './utils/isCompatible';
import { isRepoManifestBlocked } from './utils/blockedVersions';
import { AppType } from '../../models/Setting';
const logger = Logger.create('RepositoryApi');
@@ -93,14 +92,6 @@ export default class RepositoryApi {
}
public async initialize() {
await this.initializeManifestsOnly();
await this.loadRelease();
this.lastInitializedTime_ = Date.now();
}
// Skips the GitHub release API call, which is rate-limited and only needed to download plugins
public async initializeManifestsOnly() {
// https://github.com/joplin/plugins
// https://api.github.com/repos/joplin/plugins/releases
this.githubApiUrl_ = this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3');
@@ -111,6 +102,9 @@ export default class RepositoryApi {
this.isUsingDefaultContentUrl_ = this.contentBaseUrl_ === defaultContentBaseUrl;
await this.loadManifests();
await this.loadRelease();
this.lastInitializedTime_ = Date.now();
}
public async reinitialize() {
@@ -251,7 +245,6 @@ export default class RepositoryApi {
const manifest = manifests.find(m => m.id === pluginId);
if (!manifest) throw new Error(`No manifest for plugin ID "${pluginId}"`);
if (this.isBlockedByInstallMode(manifest)) throw new Error(`Plugin is blocked by intstallation policy. ID "${pluginId}"`);
if (isRepoManifestBlocked(manifest)) throw new Error(`This version of the plugin has been blocked: ${pluginId}@${manifest.version}`);
const fileUrl = this.assetFileUrl(manifest.id); // this.repoFileUrl(`plugins/${manifest.id}/plugin.jpl`);
const hash = md5(Date.now() + Math.random());
@@ -291,7 +284,7 @@ export default class RepositoryApi {
if (!manifest) return false;
const supportsCurrentAppVersion = compareVersions(installedVersion, manifest.version) < 0 && isCompatible(this.appVersion_, this.appType_, manifest);
return supportsCurrentAppVersion && !this.isBlockedByInstallMode(manifest) && !isRepoManifestBlocked(manifest);
return supportsCurrentAppVersion && !this.isBlockedByInstallMode(manifest);
}
}
@@ -254,34 +254,4 @@ describe('loadPlugins', () => {
// The bad plugin should not be running
expect(failingPluginRunner.runningPluginIds).not.toContain(badPluginId);
});
test('should stop and not run blocked plugin versions', async () => {
const blockedId = 'joplin.test.plugin.blocked';
const otherId = 'joplin.test.plugin.other';
await createTestPlugin({ ...defaultManifestProperties, id: blockedId, name: 'Blocked' });
await createTestPlugin({ ...defaultManifestProperties, id: otherId, name: 'Other' });
const pluginRunner = new MockPluginRunner();
const loadPluginsOptions: LoadPluginsProps = {
pluginRunner,
pluginSettings: Setting.value('plugins.states'),
platformImplementation,
store: createMockReduxStore(),
reloadAll: false,
cancelEvent: { cancelled: false },
};
await loadPlugins(loadPluginsOptions);
await pluginRunner.waitForAllToBeRunning([blockedId, otherId]);
Setting.setValue('plugins.blockedVersions', { [blockedId]: [defaultManifestProperties.version] });
await withWarningSilenced(/Plugin version has been blocked/, async () => {
await loadPlugins(loadPluginsOptions);
});
expect(pluginRunner.runningPluginIds).toEqual([otherId]);
const blockedManifest = PluginService.instance().pluginById(blockedId).manifest;
expect(PluginService.instance().isCompatible(blockedManifest)).toBe(false);
expect(PluginService.instance().describeIncompatibility(blockedManifest)).toContain('security reasons');
});
});
@@ -1,31 +0,0 @@
import Setting from '../../models/Setting';
import shim from '../../shim';
import PluginService from './PluginService';
import RepositoryApi from './RepositoryApi';
import { blockedVersionsFromManifests, mergeBlockedVersions } from './utils/blockedVersions';
const hasUserInstalledPlugins = async () => {
const pluginDir = Setting.value('pluginDir');
if (!(await shim.fsDriver().exists(pluginDir))) return false;
return (await shim.fsDriver().readDirStats(pluginDir)).length > 0;
};
// Returns the IDs of the running plugins that are now blocked
const refreshBlockedPluginVersions = async (repoApi: RepositoryApi) => {
if (!(await hasUserInstalledPlugins())) return [];
await repoApi.initializeManifestsOnly();
const current = Setting.value('plugins.blockedVersions') ?? {};
let fetched = blockedVersionsFromManifests(await repoApi.manifests());
if (!repoApi.isUsingDefaultContentUrl) fetched = mergeBlockedVersions(current, fetched);
// Avoids a needless settings change, which on mobile reloads all plugins
if (JSON.stringify(fetched) !== JSON.stringify(current)) Setting.setValue('plugins.blockedVersions', fetched);
const service = PluginService.instance();
return service.pluginIds.filter(id => {
const plugin = service.pluginById(id);
return plugin.running && service.isBlocked(plugin.manifest);
});
};
export default refreshBlockedPluginVersions;
@@ -1,49 +0,0 @@
import { isValidBlockedVersionEntry, isVersionBlocked, mergeBlockedVersions } from './blockedVersions';
describe('blockedVersions', () => {
test.each([
[['1.2.3'], '1.2.3', true],
[['1.2.3'], '1.2.4', false],
[['>=1.2.3'], '1.2.2', false],
[['>=1.2.3'], '1.2.4', true],
[['>=1.2.3'], '2.0.0', true],
[['1.2.3', '1.2.4'], '1.2.5', false],
[['*'], '0.0.1', true],
[['not a version'], '1.2.3', true],
[[], '1.2.3', false],
[undefined, '1.2.3', false],
])('should tell if a version is blocked (%j, %s)', (blockedVersions, version, expected) => {
expect(isVersionBlocked(blockedVersions, version)).toBe(expected);
});
test.each([
['*', true],
['1.2.3', true],
['>=1.2.3', true],
['>=1.2.3 <1.2.5', true],
['1.2.3 - 1.2.4', true],
['1.2.3 || >=2.0.0', true],
['<1.0.0 || garbage', false],
['>=1.2.3 garbage', false],
['>=1.2.3\t<2.0.0', false],
['1.2', false],
['v1.2.3', false],
['>=x', false],
['', false],
[123, false],
])('should validate blocked version entries (%j)', (entry, expected) => {
expect(isValidBlockedVersionEntry(entry)).toBe(expected);
});
test('should only add blocks when merging', () => {
const current = { 'plugin.a': ['>=1.2.3'], 'plugin.b': ['*'] };
const fetched = { 'plugin.a': ['1.2.3', '1.2.4'], 'plugin.c': ['2.0.0'] };
expect(mergeBlockedVersions(current, fetched)).toEqual({
'plugin.a': ['>=1.2.3', '1.2.3', '1.2.4'],
'plugin.b': ['*'],
'plugin.c': ['2.0.0'],
});
});
});
@@ -1,54 +0,0 @@
import { satisfies, validateStrict } from 'compare-versions';
import { PluginManifest } from './types';
// Maps a plugin ID to its blocked versions. Each entry is "*", an exact version, or a semver range
// such as ">=1.2.3", so that versions published by an attacker after the block are also covered.
export type BlockedPluginVersions = Record<string, string[]>;
const entryMatches = (entry: string, version: string) => {
if (entry === '*') return true;
try {
return satisfies(version, entry);
} catch (error) {
// Fail closed: a malformed entry still blocks the plugin
return true;
}
};
export const isVersionBlocked = (blockedVersions: string[] | undefined, version: string) => {
if (!Array.isArray(blockedVersions)) return false;
return blockedVersions.some(entry => entryMatches(entry, version));
};
// Each comparator is checked on its own because satisfies() short-circuits, so "<1.0.0 || garbage"
// would pass. This also rejects partial versions such as "1.2", which silently match nothing.
// Only spaces are accepted as separators since satisfies() throws on tabs.
export const isValidBlockedVersionEntry = (entry: unknown) => {
if (typeof entry !== 'string') return false;
if (entry === '*') return true;
const comparators = entry.split('||').flatMap(part => part.trim().split(/ +- +| +/));
return comparators.every(c => validateStrict(c.replace(/^(<=|>=|<|>|=|\^|~)/, '')));
};
export const isRepoManifestBlocked = (manifest: PluginManifest) => {
return isVersionBlocked(manifest._blocked_versions, manifest.version);
};
// A mirror may serve a stale copy of the manifests, so it can add blocks but never lift them
export const mergeBlockedVersions = (current: BlockedPluginVersions, fetched: BlockedPluginVersions) => {
const output = { ...current };
for (const [pluginId, versions] of Object.entries(fetched)) {
output[pluginId] = [...new Set([...(output[pluginId] ?? []), ...versions])];
}
return output;
};
export const blockedVersionsFromManifests = (manifests: PluginManifest[]) => {
const output: BlockedPluginVersions = {};
for (const manifest of manifests) {
if (Array.isArray(manifest._blocked_versions) && manifest._blocked_versions.length) {
output[manifest.id] = manifest._blocked_versions.filter(v => typeof v === 'string');
}
}
return output;
};
@@ -40,5 +40,4 @@ export interface PluginManifest {
_npm_package_name?: string;
_obsolete?: boolean;
_recommended?: boolean;
_blocked_versions?: string[];
}
+5 -1
View File
@@ -6,7 +6,11 @@
"description": "Used to import a OneNote archive into Joplin",
"version": "3.7.3",
"license": "MPL-2.0-no-copyleft-exception",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/onenote-converter",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/onenote-converter"
},
"main": "./renderer/pkg/renderer.js",
"scripts": {
"build": "node ./tools/build.js --profile=release",
@@ -1,4 +1,4 @@
import { applyManifestOverrides, getObsoleteManifests, getSupersededPackages, ManifestOverrides, validateManifestOverrides } from './overrideUtils';
import { applyManifestOverrides, getObsoleteManifests, getSupersededPackages, ManifestOverrides } from './overrideUtils';
describe('overrideUtils', () => {
@@ -104,23 +104,4 @@ describe('overrideUtils', () => {
const supersededPackages = getSupersededPackages(manifestOverrides);
expect(supersededPackages).toMatchObject(['joplin-plugin-benji-favorites']);
});
test.each([
[['>=1.2.3', '1.0.0'], true],
[['1.2'], false],
[[] as string[], false],
['1.2.3', false],
])('should validate blocked versions (%j)', (blockedVersions, isValid) => {
const overrides = { 'org.joplinapp.plugins.ToggleSidebars': { _blocked_versions: blockedVersions } } as ManifestOverrides;
if (isValid) {
expect(() => validateManifestOverrides(overrides)).not.toThrow();
} else {
expect(() => validateManifestOverrides(overrides)).toThrow('Invalid _blocked_versions');
}
});
test('should not allow blocking a plugin that is not in the manifests', () => {
const overrides: ManifestOverrides = { 'does.not.exist': { _blocked_versions: ['*'] } };
expect(() => applyManifestOverrides({}, overrides)).toThrow('Cannot block plugin does.not.exist');
});
});
+1 -19
View File
@@ -1,13 +1,11 @@
import * as path from 'path';
import { readJsonFile } from './utils';
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
import { isValidBlockedVersionEntry } from '@joplin/lib/services/plugins/utils/blockedVersions';
export interface ManifestOverride {
_obsolete?: boolean;
_recommended?: boolean;
_superseded_package?: string;
_blocked_versions?: string[];
}
export type ManifestOverrides = Record<string, ManifestOverride>;
@@ -18,10 +16,6 @@ export function applyManifestOverrides(manifests: PluginManifests, overrides: Ma
const manifest = manifests[pluginId] as PluginManifest & Record<string, unknown>;
if (!manifest) {
if (override._blocked_versions) {
throw new Error(`Cannot block plugin ${pluginId}: it is not in manifests.json`);
}
// If the manifest does not exist in the destination, it means the
// plugin has been taken out, so the obsolete property should be set
// to `true`. If not, it's an error.
@@ -55,20 +49,8 @@ function pluginManifestOverridesPath(repoDir: string): string {
return path.resolve(repoDir, 'manifestOverrides.json');
}
export const validateManifestOverrides = (overrides: ManifestOverrides) => {
for (const [pluginId, override] of Object.entries(overrides)) {
const blockedVersions = override._blocked_versions;
if (blockedVersions === undefined) continue;
if (!Array.isArray(blockedVersions) || !blockedVersions.length || !blockedVersions.every(isValidBlockedVersionEntry)) {
throw new Error(`Invalid _blocked_versions for plugin ${pluginId}: ${JSON.stringify(blockedVersions)}. It must be a non-empty array of "*", exact versions or semver ranges.`);
}
}
};
export async function readManifestOverrides(repoDir: string): Promise<ManifestOverrides> {
const overrides: ManifestOverrides = await readJsonFile(pluginManifestOverridesPath(repoDir), {});
validateManifestOverrides(overrides);
return overrides;
return readJsonFile(pluginManifestOverridesPath(repoDir), {});
}
export const getSupersededPackages = (manifestOverrides: ManifestOverrides): string[] => {
@@ -105,19 +105,6 @@ describe('validateUntrustedManifest', () => {
).toThrow('mark itself as recommended');
});
test('should not allow plugin authors to set their own blocked versions', () => {
const newManifest = {
id: 'joplin-plugin.this.is.another.test',
_blocked_versions: [] as string[],
_npm_package_name: 'joplin-plugin-another-test',
version: '1.2.3',
};
expect(
() => validateUntrustedManifest(newManifest as unknown as PluginManifest, originalManifests, { source: 'npm' }),
).toThrow('cannot set its own blocked versions');
});
test('should not allow changing the NPM package for an existing plugin', () => {
const newManifest = {
id: 'joplin-plugin.this.is.a.test',
@@ -26,10 +26,6 @@ const validateUntrustedManifest = (
throw new Error(`Plugin ${manifest.id} cannot mark itself as recommended.`);
}
if (typeof manifest._blocked_versions !== 'undefined') {
throw new Error(`Plugin ${manifest.id} cannot set its own blocked versions.`);
}
if (options.source === 'repository') {
if (!manifest.repository_url?.trim()) {
throw new Error(`Plugin ${manifest.id} must specify repository_url when published from a repository.`);
+5
View File
@@ -2,6 +2,11 @@
"name": "@joplin/plugin-repo-cli",
"version": "3.7.6",
"description": "",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/plugin-repo-cli"
},
"main": "./dist/index.js",
"bin": "./dist/index.js",
"publishConfig": {
+5 -1
View File
@@ -36,7 +36,11 @@
"storage-access-framework",
"fs"
],
"repository": "https://github.com/jd1378/react-native-saf-x",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/react-native-saf-x"
},
"author": "Javad Mnjd (https://github.com/jd1378)",
"license": "AGPL-3.0-or-later",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/react-native-saf-x",
+5 -1
View File
@@ -2,7 +2,11 @@
"name": "@joplin/renderer",
"version": "3.7.3",
"description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/renderer"
},
"main": "index.js",
"types": "index.d.ts",
"publishConfig": {
+2 -1
View File
@@ -17,7 +17,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git"
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/tools"
},
"license": "AGPL-3.0-or-later",
"dependencies": {
+41
View File
@@ -0,0 +1,41 @@
import { bumpVersions, PackageJson, sortByDependencies } from './releaseSubPackages';
const toPackages = (manifests: PackageJson[]) => manifests.map(manifest => ({ dir: '', manifest }));
describe('releaseSubPackages', () => {
test('should bump all versions and update ranges between public packages', () => {
const result = bumpVersions([
{ name: 'utils', version: '3.7.3' },
{ name: 'fork', version: '4.1.66' },
{
name: 'lib',
version: '3.7.3',
dependencies: { 'utils': '^3.7.3', 'fork': '~4.1.60', 'external': '^3.7.3' },
devDependencies: { 'utils': '~3.7', 'other': '1.0.0' },
},
]);
expect(result.map(m => m.version)).toEqual(['3.7.4', '4.1.67', '3.7.4']);
expect(result[2].dependencies).toEqual({ 'utils': '^3.7.4', 'fork': '~4.1.67', 'external': '^3.7.3' });
expect(result[2].devDependencies).toEqual({ 'utils': '~3.7', 'other': '1.0.0' });
});
test('should sort packages so that dependencies come first', () => {
const result = sortByDependencies(toPackages([
{ name: 'tools', version: '1.0.0', dependencies: { 'lib': '^1.0.0' } },
{ name: 'lib', version: '1.0.0', dependencies: { 'utils': '^1.0.0' } },
{ name: 'utils', version: '1.0.0', devDependencies: { 'tools': '^1.0.0' } },
]));
expect(result.map(p => p.manifest.name)).toEqual(['utils', 'lib', 'tools']);
});
test('should throw on circular dependencies', () => {
expect(() => sortByDependencies(toPackages([
{ name: 'a', version: '1.0.0', dependencies: { 'b': '^1.0.0' } },
{ name: 'b', version: '1.0.0', dependencies: { 'a': '^1.0.0' } },
]))).toThrow('Circular dependency');
});
});
+165
View File
@@ -0,0 +1,165 @@
import { execCommand, getRootDir } from '@joplin/utils';
import { satisfies } from 'compare-versions';
import { copy, pathExists, readdir, readFile, remove, writeFile } from 'fs-extra';
import { join } from 'path';
import { chdir } from 'process';
import yargs = require('yargs');
import { hideBin } from 'yargs/helpers';
type Dependencies = Record<string, string>;
export interface PackageJson {
name: string;
version: string;
private?: boolean;
dependencies?: Dependencies;
devDependencies?: Dependencies;
peerDependencies?: Dependencies;
optionalDependencies?: Dependencies;
}
export interface PackageInfo {
dir: string;
manifest: PackageJson;
}
const dependencyTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const;
export const bumpPatch = (version: string) => {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) throw new Error(`Unsupported version format: ${version}`);
return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
};
// Same behaviour as `lerna version --no-private`: every public package gets a patch bump, and ranges
// pointing to another public package are moved to its new version, keeping the `^` or `~` prefix.
export const bumpVersions = (manifests: PackageJson[]) => {
const oldVersions = new Map(manifests.map(manifest => [manifest.name, manifest.version]));
const updateRange = (name: string, range: string) => {
const oldVersion = oldVersions.get(name);
const match = range.match(/^([\^~]?)\d+\.\d+\.\d+$/);
if (!oldVersion || !match || !satisfies(oldVersion, range)) return range;
return `${match[1]}${bumpPatch(oldVersion)}`;
};
return manifests.map(manifest => {
const output: PackageJson = { ...manifest, version: bumpPatch(manifest.version) };
for (const type of dependencyTypes) {
const dependencies = manifest[type];
if (!dependencies) continue;
output[type] = Object.fromEntries(Object.entries(dependencies).map(([name, range]) => [name, updateRange(name, range)]));
}
return output;
});
};
// Dev dependencies are ignored because they don't affect consumers, and including them could create
// cycles.
export const sortByDependencies = (packages: PackageInfo[]) => {
const remaining = new Map(packages.map(p => [p.manifest.name, p]));
const output: PackageInfo[] = [];
const runtimeDependencies = ({ manifest }: PackageInfo) => {
return Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies, ...manifest.optionalDependencies });
};
while (remaining.size) {
const ready = [...remaining.values()].filter(p => !runtimeDependencies(p).some(name => remaining.has(name)));
if (!ready.length) throw new Error(`Circular dependency between: ${[...remaining.keys()].join(', ')}`);
for (const p of ready) {
output.push(p);
remaining.delete(p.manifest.name);
}
}
return output;
};
const loadPublicPackages = async (rootDir: string) => {
const packagesDir = join(rootDir, 'packages');
const output: PackageInfo[] = [];
for (const name of await readdir(packagesDir)) {
const dir = join(packagesDir, name);
const path = join(dir, 'package.json');
if (!await pathExists(path)) continue;
const manifest: PackageJson = JSON.parse(await readFile(path, 'utf8'));
if (!manifest.private) output.push({ dir, manifest });
}
return output;
};
const isPublished = async ({ name, version }: PackageJson) => {
try {
return await execCommand(['npm', 'view', `${name}@${version}`, 'version'], { quiet: true }) !== '';
} catch (error) {
if (String(error).includes('E404')) return false;
throw error;
}
};
const hasLicenseFile = async (dir: string) => {
return (await readdir(dir)).some(name => /^(license|licence)(\..*)?$/i.test(name));
};
// Like lerna, the root licence is temporarily copied to packages that don't have their own.
const publishPackage = async (dir: string, rootLicensePath: string, dryRun: boolean) => {
const tempLicensePath = await hasLicenseFile(dir) ? '' : join(dir, 'LICENSE');
if (tempLicensePath) await copy(rootLicensePath, tempLicensePath);
const previousDir = process.cwd();
try {
chdir(dir);
// Without --workspaces=false, npm applies the root .gitignore to the package, which excludes
// required files such as packages/lib/vendor/
await execCommand(['npm', 'publish', '--workspaces=false', ...(dryRun ? ['--dry-run'] : [])]);
} finally {
chdir(previousDir);
if (tempLicensePath) await remove(tempLicensePath);
}
};
const versionAll = async () => {
const packages = await loadPublicPackages(await getRootDir());
const bumped = bumpVersions(packages.map(p => p.manifest));
for (const [index, { dir, manifest }] of packages.entries()) {
await writeFile(join(dir, 'package.json'), `${JSON.stringify(bumped[index], null, 2)}\n`, 'utf8');
console.info(`${manifest.name}: ${manifest.version} => ${bumped[index].version}`);
}
};
// Packages are published one at a time, dependencies first, so that a failure leaves the registry in a
// consistent state. Already published versions are skipped, so the command can be re-run after a failure.
const publishAll = async (dryRun: boolean) => {
const rootDir = await getRootDir();
for (const { dir, manifest } of sortByDependencies(await loadPublicPackages(rootDir))) {
const id = `${manifest.name}@${manifest.version}`;
if (await isPublished(manifest)) {
console.info(`Skipping ${id}: already published`);
continue;
}
console.info(`Publishing ${id}...`);
await publishPackage(dir, join(rootDir, 'LICENSE'), dryRun);
}
};
if (require.main === module) {
void (async () => {
try {
await yargs(hideBin(process.argv))
.command('version', 'Bumps the patch version of all public packages', {}, () => versionAll())
.command('publish', 'Publishes all public packages whose version is not on the registry yet', yargs => {
return yargs.option('dry-run', {
type: 'boolean',
default: false,
});
}, args => publishAll(args['dry-run']))
.demandCommand(1)
.strict()
.parseAsync();
} catch (error) {
console.error('Fatal error');
console.error(error);
process.exit(1);
}
})();
}
+2 -1
View File
@@ -30,7 +30,8 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/laurent22/joplin-turndown-plugin-gfm.git"
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/turndown-plugin-gfm"
},
"scripts": {
"build-all": "npm run build-cjs && npm run build-es && npm run build-iife",
+1 -1
View File
@@ -34,7 +34,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/laurent22/joplin.git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/turndown"
},
"scripts": {
+5 -1
View File
@@ -2,7 +2,11 @@
"name": "@joplin/utils",
"version": "3.7.3",
"description": "Utilities for Joplin",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/utils",
"repository": {
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/utils"
},
"exports": {
".": {
"types": "./index.ts",
+3 -1
View File
@@ -34,7 +34,9 @@ Unlike the mobile or desktop application, the CLI app doesn't bundle its depende
yarn publishAll
This is going to publish all the Joplin libraries, such as `@joplin/lib`, `@joplin/tools`, etc.
This is going to bump the version of all the Joplin libraries, such as `@joplin/lib`, `@joplin/tools`, etc., then commit and push a `sub-packages-v*` tag. The tag triggers the `publish-sub-packages.yml` workflow, which builds and publishes the libraries to npm using [trusted publishing](https://docs.npmjs.com/trusted-publishers). Wait for that workflow to complete before releasing the CLI app.
Each library must have `laurent22/joplin` and `publish-sub-packages.yml` registered as a trusted publisher on npmjs.com, with "Allow npm publish" enabled. When adding a new library, it must be published once manually before this can be configured.
Then in `app-cli/package.json`, all `@joplin` dependencies and devdependencies must be set to the last major/minor version. For example:
+93 -3309
View File
File diff suppressed because it is too large. Load diff