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

Conversation

mak2002
Copy link
Contributor

@mak2002 mak2002 commented Jul 19, 2022

Summary

This PR implements bundling plugins with desktop app. When the app is being built, we will run a JS file that pulls the jpl files from the NPM registry and then moves it in build folder of the app. In this script, we are providing default plugins IDs with their versions that we intend to use.

echo "Step: Bundling default plugins in desktop application"
cd "$ROOT_DIR/packages/tools"
tsc bundleDefaultPlugins.ts
node bundleDefaultPlugins.js
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code is only necessary when building the desktop app, but you have it here where it will be called even when building the server image. I think this code only needs to be run for the first block below (Building the desktop application for publishing).

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 will move it then. I thought Docker Image also needs these plugins.

Comment on lines 174 to 175
tsc bundleDefaultPlugins.ts
node bundleDefaultPlugins.js
Copy link
Owner

Choose a reason for hiding this comment

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

yarn run build runs before getting here so this TS file should already been compiled, so please remove the tsc statement

import fs = require('fs-extra');
const fetch = require('node-fetch');

describe('bundlePlugins', function() {
Copy link
Owner

Choose a reason for hiding this comment

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

Should be named like the file bundleDefaultPlugins

describe('bundlePlugins', 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.


let manifests = [];
try {
const manifestData = await fetch('https://raw.githubusercontent.com/joplin/plugins/master/manifests.json');
Copy link
Owner

Choose a reason for hiding this comment

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

Is it actually downloading stuff off GitHub? We can't have this, you'll need to mock this. In general all tests should be able to run offline.

Comment on lines 69 to 75
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);
}
Copy link
Owner

Choose a reason for hiding this comment

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

Something's not right here, it shouldn't log anything in tests. You need to test that it doesn't throw, if it's not supposed to throw. And in fact I think the tests themselves shouldn't throw exception (the code they call might).

const downloadFile = async (url: string, name: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error(`Unexpected response ${response.statusText}`);
await streamPipeline(response.body, fs.createWriteStream(name));
Copy link
Owner

Choose a reason for hiding this comment

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

Is it not possible to use fetch? Too many problems with streams in general

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 think we can't use fetch alone, we have to write the response to a file. If we don't want to use streams then we can use the following alternative:

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


import path = require('path');
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 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

describe('bundlePlugins', 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?

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 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

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

};

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

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

@@ -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.

Copy link
Owner

@laurent22 laurent22 left a comment

Choose a reason for hiding this comment

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

Nearly there I think, just a few more tweaks

}
}

export const downloadPlugins = async (localPluginsVersions: PluginAndVersion, defaultPluginsInfo: DefaultPluginsInfo, manifests: any): Promise<any> => {
Copy link
Owner

Choose a reason for hiding this comment

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

Shouldn't return any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this with a type.

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

if (!await pathExists(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.

You don't need this check, because downloadFile should throw if there's an error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Thanks for catching this.

packages/tools/bundleDefaultPlugins.ts Show resolved Hide resolved

const downloadedPluginsNames: PluginIdAndName = {};
for (const pluginId of Object.keys(defaultPluginsInfo)) {
if (localPluginsVersions[pluginId] === defaultPluginsInfo[pluginId].version) 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 download plugin file ${responseText.substr(0,500)}`);
throw new Error(`Cannot fetch ${manifests[pluginId]._npm_package_name} release info from NPM`);
Copy link
Owner

Choose a reason for hiding this comment

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

Actually your previous code was good, as you were showing the actual error message. Any reason you removed it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, while testing locally I thought that .text() will show buffer data but on failing to fetch it won't have any buffer data to show 😅 . Restored the previous message.

@@ -32,10 +32,13 @@ export const localPluginsVersion = async (defaultPluginDir: string, defaultPlugi

async function downloadFile(url: string, outputPath: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Cannot download file from ${url}`);
Copy link
Owner

Choose a reason for hiding this comment

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

Here too you might want to provide more info such as response.status or response.text

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 👍

@laurent22
Copy link
Owner

Looks good now, thanks @mak2002 and @CalebJohn!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants