Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Bundle default plugins with desktop application #6679

Merged
merged 20 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,12 @@ packages/tools/buildServerDocker.js.map
packages/tools/buildServerDocker.test.d.ts
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.test.js.map
packages/tools/bundleDefaultPlugins.d.ts
packages/tools/bundleDefaultPlugins.js
packages/tools/bundleDefaultPlugins.js.map
packages/tools/bundleDefaultPlugins.test.d.ts
packages/tools/bundleDefaultPlugins.test.js
packages/tools/bundleDefaultPlugins.test.js.map
packages/tools/checkLibPaths.d.ts
packages/tools/checkLibPaths.js
packages/tools/checkLibPaths.js.map
Expand Down
2 changes: 2 additions & 0 deletions .github/scripts/run_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ cd "$ROOT_DIR/packages/app-desktop"

if [[ $GIT_TAG_NAME = v* ]]; then
echo "Step: Building and publishing desktop application..."
cd "$ROOT_DIR/packages/tools"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 175 Is meant to be run form the packages/app-desktop directory, but this line places the script in packages/tools. This appears like it will break the ci build (for releases).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I will fix it.

node bundleDefaultPlugins.js
USE_HARD_LINKS=false yarn run dist
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
echo "Step: Building Docker Image..."
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,12 @@ packages/tools/buildServerDocker.js.map
packages/tools/buildServerDocker.test.d.ts
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.test.js.map
packages/tools/bundleDefaultPlugins.d.ts
packages/tools/bundleDefaultPlugins.js
packages/tools/bundleDefaultPlugins.js.map
packages/tools/bundleDefaultPlugins.test.d.ts
packages/tools/bundleDefaultPlugins.test.js
packages/tools/bundleDefaultPlugins.test.js.map
packages/tools/checkLibPaths.d.ts
packages/tools/checkLibPaths.js
packages/tools/checkLibPaths.js.map
Expand Down
Binary file not shown.
103 changes: 103 additions & 0 deletions packages/app-cli/tests/services/plugins/mockData/mockResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { supportDir } from '@joplin/lib/testing/test-utils';
import path = require('path');
const fs = require('fs');

const response1 = {
'_id': 'joplin-plugin-rich-markdown',
'name': 'joplin-plugin-rich-markdown',
'versions': {
'0.8.2': {
'name': 'joplin-plugin-rich-markdown',
'version': '0.8.2',
'description': 'A plugin that will finally allow you to ditch the markdown viewer, saving space and making your life easier.',
'_id': '[email protected]',
'dist': {
'tarball': 'no-link-here',
},
},
'0.9.0': {
'name': 'joplin-plugin-rich-markdown',
'version': '0.9.0',
'dist': {
'tarball': `${path.join(supportDir, '..', 'services', 'plugins', 'mockData', 'richMarkdown.tgz')}`,
},
},
},
};

const response2 = {
'_id': 'io.github.jackgruber.backup',
'name': 'joplin-plugin-rich-markdown',
'versions': {
'1.0.0': {
'name': 'joplin-plugin-rich-markdown',
'version': '1.0.0',
'description': 'A plugin that will finally allow you to ditch the markdown viewer, saving space and making your life easier.',
'_id': '[email protected]',
'dist': {
'tarball': 'no-link-here',
},
},
'1.1.0': {
'name': 'joplin-plugin-rich-markdown',
'version': '1.1.0',
'dist': {
'tarball': `${path.join(supportDir, '..', 'services', 'plugins', 'mockData', 'simpleBackup.tgz')}`,
},
},
},
};

export const manifests = {
'io.github.jackgruber.backup': {
'manifest_version': 1,
'id': 'io.github.jackgruber.backup',
'app_min_version': '2.1.3',
'version': '1.1.0',
'name': 'Simple Backup',
'description': 'Plugin to create manual and automatic backups.',
'author': 'JackGruber',
'homepage_url': 'https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md',
'repository_url': 'https://github.com/JackGruber/joplin-plugin-backup',
'keywords': [
'backup',
'jex',
'export',
'zip',
'7zip',
'encrypted',
],
'_publish_hash': 'sha256:8d8c6a3bb92fafc587269aea58b623b05242d42c0766a05bbe25c3ba2bbdf8ee',
'_publish_commit': 'master:00ed52133c659e0f3ac1a55f70b776c42fca0a6d',
'_npm_package_name': 'joplin-plugin-backup',
},
'plugin.calebjohn.rich-markdown': {
'manifest_version': 1,
'id': 'plugin.calebjohn.rich-markdown',
'app_min_version': '2.7',
'version': '0.9.0',
'name': 'Rich Markdown',
'description': 'Helping you ditch the markdown viewer for good.',
'author': 'Caleb John',
'homepage_url': 'https://github.com/CalebJohn/joplin-rich-markdown#readme',
'repository_url': 'https://github.com/CalebJohn/joplin-rich-markdown',
'keywords': [
'editor',
'visual',
],
'_publish_hash': 'sha256:95337a3868aebdc9bf8c347a37460d0c2753b391ff51a0c72bdccdef9679705f',
'_publish_commit': 'main:af3493b6ca96c931327ab3bd04906faaed0c782c',
'_npm_package_name': 'joplin-plugin-rich-markdown',
},

};

export async function mockFile() {
const filePath = path.join(__dirname, 'richMarkdown.tgz');
const someFile = await fs.readFileSync(filePath, 'utf8');
return { buffer: () => someFile };
}

export const NPM_Response1 = JSON.stringify(response1);
export const NPM_Response2 = JSON.stringify(response2);

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"manifest_version": 1,
"id": "io.github.jackgruber.backup",
"app_min_version": "2.1.3",
"version": "1.1.0",
"name": "Simple Backup",
"description": "Plugin to create manual and automatic backups.",
"author": "JackGruber",
"homepage_url": "https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md",
"repository_url": "https://github.com/JackGruber/joplin-plugin-backup",
"keywords": [
"backup",
"jex",
"export",
"zip",
"7zip",
"encrypted"
],
"_publish_hash": "sha256:8d8c6a3bb92fafc587269aea58b623b05242d42c0766a05bbe25c3ba2bbdf8ee",
"_publish_commit": "master:00ed52133c659e0f3ac1a55f70b776c42fca0a6d",
"_npm_package_name": "joplin-plugin-backup"
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"manifest_version": 1,
"id": "plugin.calebjohn.rich-markdown",
"app_min_version": "2.7",
"version": "0.9.0",
"name": "Rich Markdown",
"description": "Helping you ditch the markdown viewer for good.",
"author": "Caleb John",
"homepage_url": "https://github.com/CalebJohn/joplin-rich-markdown#readme",
"repository_url": "https://github.com/CalebJohn/joplin-rich-markdown",
"keywords": [
"editor",
"visual"
],
"_publish_hash": "sha256:95337a3868aebdc9bf8c347a37460d0c2753b391ff51a0c72bdccdef9679705f",
"_publish_commit": "main:af3493b6ca96c931327ab3bd04906faaed0c782c",
"_npm_package_name": "joplin-plugin-rich-markdown"
}
Binary file not shown.
87 changes: 87 additions & 0 deletions packages/tools/bundleDefaultPlugins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@



import path = require('path');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only import the functions you need

import { downloadPlugins, localPluginsVersion } from './bundleDefaultPlugins';
import fs = require('fs-extra');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't import * only import the functions you need

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted 👍

import { mockFile, NPM_Response1 } from '../app-cli/tests/services/plugins/mockData/mockResponses';
// import { supportDir } from '@joplin/lib/testing/test-utils';
import { manifests } from '../app-cli/tests/services/plugins/mockData/mockResponses';
// const {Response} = jest.requireActual('node-fetch');

const fetch = require('node-fetch');

jest.mock('node-fetch', ()=>jest.fn());

describe('bundleDefaultPlugins', function() {

beforeEach(() => {
jest.setTimeout(20000);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this setTimeout for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using this just to increase the timeout for tests on my local system, otherwise they seem to fail randomly. This will be removed before merging the code.

});

const testDefaultPluginsIds = {
'plugin.calebjohn.rich-markdown': '0.9.0',
'io.github.jackgruber.backup': '1.1.0',
};


// test('it should get local plugin versions', async () => {
// const manifestsPath = path.join(__dirname, '..', '..', '..' , 'joplin/packages/app-cli/tests/services/testPlugins');

// const localPluginsVersions = await localPluginsVersion(manifestsPath, testDefaultPluginsIds);

// expect(localPluginsVersions['io.github.jackgruber.backup']).toBe('1.1.0');
// expect(localPluginsVersions['plugin.calebjohn.rich-markdown']).toBe('0.9.0');
// });

it('it should download plugins folder from GitHub with no initial plugins', async () => {

const mockTGZ = await mockFile();
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
mockFetch.mockResolvedValueOnce({ text: () => Promise.resolve(NPM_Response1), ok: true })
.mockResolvedValueOnce({ ...mockTGZ, ok: true });

const tempDir = path.join(__dirname, '/tempDownload');

const localPluginsVersions = { 'io.github.jackgruber.backup': '0.0.0', 'plugin.calebjohn.rich-markdown': '0.0.0' };

await fs.mkdirp(`${tempDir}`);
await downloadPlugins(tempDir, localPluginsVersions, testDefaultPluginsIds, manifests);

expect(fs.existsSync(`${tempDir}/io.github.jackgruber.backup`)).toBe(true);
expect(fs.existsSync(`${tempDir}/plugin.calebjohn.rich-markdown`)).toBe(true);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only use async methods


const localPluginsVersions2 = await localPluginsVersion(tempDir, testDefaultPluginsIds);

expect(localPluginsVersions2['plugin.calebjohn.rich-markdown']).toBe('0.9.0');
expect(localPluginsVersions2['io.github.jackgruber.backup']).toBe('1.1.0');

await fs.remove(tempDir);
});

// test('it should download plugins folder from GitHub with initial plugins', async () => {

// const tempDir = path.join(__dirname, '/tempDownload');
// await fs.remove(tempDir);

// let manifests = [];
// try {
// const manifestData = await fetch('https://raw.githubusercontent.com/joplin/plugins/master/manifests.json');
// manifests = JSON.parse(await manifestData.text());
// if (!manifests) throw new Error('Invalid or missing JSON');
// } catch (error) {
// console.log(error);
// }

// const localPluginsVersions = { 'io.github.jackgruber.backup': '1.1.0', 'plugin.calebjohn.rich-markdown': '0.0.0' };

// await fs.mkdirp(`${tempDir}`);
// await downloadPlugins(tempDir, localPluginsVersions, testDefaultPluginsIds, manifests);

// expect(fs.existsSync(`${tempDir}/io.github.jackgruber.backup`)).toBe(false);
// expect(fs.existsSync(`${tempDir}/plugin.calebjohn.rich-markdown`)).toBe(true);

// await fs.remove(tempDir);
// });

});
80 changes: 80 additions & 0 deletions packages/tools/bundleDefaultPlugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import path = require('path');
import { execCommand2 } from './tool-utils';
import fs = require('fs-extra');
import { defaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
const fetch = require('node-fetch');
const { writeFile } = require('fs');
const { promisify } = require('util');
const writeFilePromise = promisify(writeFile);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just import from fs-extra, it's already promisified

// const util = require('util');

interface PluginAndVersion {
[pluginId: string]: string;
}

export const localPluginsVersion = async (defaultPluginDir: string, defaultPluginsId: PluginAndVersion): Promise<PluginAndVersion> => {
if (!await fs.pathExists(path.join(defaultPluginDir))) await fs.mkdir(defaultPluginDir);
const localPluginsVersions: PluginAndVersion = {};

for (const pluginId of Object.keys(defaultPluginsId)) {

if (!await fs.pathExists(path.join(__dirname, '..', '..', `packages/app-desktop/build/defaultPlugins/${pluginId}`))) {
localPluginsVersions[pluginId] = '0.0.0';
continue;
}
const data = fs.readFileSync(`${defaultPluginDir}/${pluginId}/manifest.json`, 'utf8');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only async calls

const manifest = JSON.parse(data);
localPluginsVersions[pluginId] = manifest.version;
}
return localPluginsVersions;
};

function downloadFile(url: string, outputPath: string) {
return fetch(url).then(response => response.buffer()).then(buff => writeFilePromise(outputPath, Buffer.from(buff)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no then, only await/async

}

export const downloadPlugins = async (defaultPluginDir: string, localPluginsVersions: PluginAndVersion, defaultPluginsId: PluginAndVersion, manifests: any): Promise<void> => {

for (const pluginId of Object.keys(defaultPluginsId)) {
if (localPluginsVersions[pluginId] === defaultPluginsId[pluginId]) continue;
const response = await fetch(`https://registry.npmjs.org/${manifests[pluginId]._npm_package_name}`);

if (!(response.ok)) {
// const responseText = await response.text();
throw new Error('Cannot get latest release info');
}
const release = JSON.parse(await response.text());

const pluginUrl = release.versions[defaultPluginsId[pluginId]].dist.tarball;

const pluginName = `${manifests[pluginId]._npm_package_name}-${defaultPluginsId[pluginId]}.tgz`;
await downloadFile(pluginUrl, pluginName);

if (!fs.existsSync(pluginName)) throw new Error(`${pluginName} cannot be downloaded`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only async code


await execCommand2(`tar xvzf ${pluginName}`);
await fs.move(`package/publish/${pluginId}.jpl`,`${defaultPluginDir}/${pluginId}/plugin.jpl`, { overwrite: true });
await fs.move(`package/publish/${pluginId}.json`,`${defaultPluginDir}/${pluginId}/manifest.json`, { overwrite: true });
await fs.remove(`${pluginName}`);
await fs.remove('package');
}
};

async function start(): Promise<void> {
const defaultPluginDir = path.join(__dirname, '..', '..', 'packages/app-desktop/build/defaultPlugins');

const manifestData = await fetch('https://raw.githubusercontent.com/joplin/plugins/master/manifests.json');
const manifests = JSON.parse(await manifestData.text());
if (!manifests) throw new Error('Invalid or missing JSON');

const localPluginsVersions = await localPluginsVersion(defaultPluginDir, defaultPlugins);
await downloadPlugins(defaultPluginDir, localPluginsVersions, defaultPlugins, manifests);
}

if (require.main === module) {
start().catch((error) => {
console.error('Fatal error');
console.error(error);
process.exit(1);
});
}