Browse Source

fix(export): fix archiver module not found error when packaged

When packaging the app, it would not load due to an "module 'archiver' not found" error. This was
caused by it being listed as an "external" in the webpack config. That webpack exclusion was
previously needed because the library wouldn't work in chromium without it. The zip/unzip logic has
now been moved to the main renderer process in order to get it working.
master
jamaljsr 5 years ago
parent
commit
d1e35f4c43
  1. 9
      .rescriptsrc.js
  2. 3
      electron/appIpcListener.ts
  3. 13
      electron/utils/zip.ts
  4. 6
      package.json
  5. 9
      src/components/network/ImportNetwork.spec.tsx
  6. 12
      src/components/network/NetworkView.spec.tsx
  7. 4
      src/shared/ipcChannels.ts
  8. 5
      src/store/models/network.spec.ts
  9. 10
      src/utils/network.ts
  10. 131
      src/utils/zip.spec.ts

9
.rescriptsrc.js

@ -7,12 +7,9 @@ module.exports = [
// add support for hot reload of hooks
config.resolve.alias['react-dom'] = '@hot-loader/react-dom';
// https://github.com/websockets/ws/issues/1538#issuecomment-577627369
config.resolve.alias['ws'] = path.resolve(path.join(__dirname, 'node_modules/ws/index.js' ))
// fix for archiver module with webpack
// See https://github.com/archiverjs/node-archiver/issues/403
config.externals = {
archiver: "require('archiver')",
};
config.resolve.alias['ws'] = path.resolve(
path.join(__dirname, 'node_modules/ws/index.js'),
);
return config;
},
[

3
electron/appIpcListener.ts

@ -6,6 +6,7 @@ import { ipcChannels } from '../src/shared';
import { APP_ROOT, BASE_URL } from './constants';
import { httpProxy } from './httpProxy';
import { clearProxyCache } from './lnd/lndProxyServer';
import { unzip, zip } from './utils/zip';
const openWindow = async (args: { url: string }): Promise<boolean> => {
debug('openWindow', args);
@ -60,6 +61,8 @@ const listeners: {
[ipcChannels.openWindow]: openWindow,
[ipcChannels.clearCache]: clearCache,
[ipcChannels.http]: httpProxy,
[ipcChannels.zip]: zip,
[ipcChannels.unzip]: unzip,
};
/**

13
src/utils/zip.ts → electron/utils/zip.ts

@ -9,9 +9,9 @@ import unzipper from 'unzipper';
* @param filePath the path to the zip file
* @param destination the folder to extract to
*/
export const unzip = (filePath: string, destination: string): Promise<void> => {
return new Promise(async (resolve, reject) => {
try {
export const unzip = (args: { filePath: string; destination: string }): Promise<any> =>
new Promise(async (resolve, reject) => {
const { filePath, destination } = args;
const exists = await pathExists(filePath);
if (!exists) {
throw Error(`${filePath} does not exist!`);
@ -25,19 +25,16 @@ export const unzip = (filePath: string, destination: string): Promise<void> => {
error(`Could not unzip ${filePath} into ${destination}:`, err);
reject(err);
});
} catch (err) {
reject(err);
}
});
};
/**
* Zips the contents of a folder
* @param source the folder path containing the files to zip
* @param destination the file path of where to store the zip
*/
export const zip = (source: string, destination: string): Promise<void> =>
export const zip = (args: { source: string; destination: string }): Promise<any> =>
new Promise(async (resolve, reject) => {
const { source, destination } = args;
info(`zipping ${source} to ${destination}`);
const archive = archiver('zip');
archive.on('finish', () => resolve());

6
package.json

@ -41,6 +41,7 @@
"dependencies": {
"@radar/lnrpc": "0.9.1-beta",
"@types/detect-port": "1.1.0",
"archiver": "3.1.1",
"detect-port": "1.3.0",
"docker-compose": "0.23.3",
"dockerode": "3.1.0",
@ -48,7 +49,9 @@
"electron-is-dev": "1.1.0",
"electron-log": "4.1.0",
"electron-window-state": "5.0.3",
"fs-extra": "9.0.0",
"shell-env": "3.0.0",
"unzipper": "0.10.10",
"xterm": "4.4.0",
"xterm-addon-fit": "0.3.0"
},
@ -83,7 +86,6 @@
"@typescript-eslint/eslint-plugin": "2.24.0",
"@typescript-eslint/parser": "2.24.0",
"antd": "4.0.3",
"archiver": "3.1.1",
"babel-plugin-emotion": "10.0.29",
"babel-plugin-import": "1.13.0",
"bitcoin-core": "3.0.0",
@ -104,7 +106,6 @@
"eslint-config-prettier": "6.10.0",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-react": "7.19.0",
"fs-extra": "9.0.0",
"history": "4.10.1",
"husky": "4.2.3",
"i18next": "19.3.3",
@ -141,7 +142,6 @@
"testcafe-react-selectors": "4.0.0",
"ts-node": "8.7.0",
"typescript": "3.8.3",
"unzipper": "0.10.10",
"utf-8-validate": "5.0.2",
"wait-on": "4.0.1",
"webpack": "4.41.5",

9
src/components/network/ImportNetwork.spec.tsx

@ -4,21 +4,21 @@ import { join } from 'path';
import { IChart } from '@mrblenny/react-flow-chart';
import { fireEvent } from '@testing-library/react';
import * as os from 'os';
import * as ipc from 'lib/ipc/ipcService';
import { Network } from 'types';
import { initChartFromNetwork } from 'utils/chart';
import * as files from 'utils/files';
import { getNetwork, renderWithProviders } from 'utils/tests';
import * as zip from 'utils/zip';
import ImportNetwork from './ImportNetwork';
jest.mock('utils/files');
jest.mock('utils/zip');
jest.mock('os');
jest.mock('lib/ipc/ipcService');
const osMock = os as jest.Mocked<typeof os>;
const fsMock = fs as jest.Mocked<typeof fs>;
const filesMock = files as jest.Mocked<typeof files>;
const zipMock = zip as jest.Mocked<typeof zip>;
const ipcMock = ipc as jest.Mocked<typeof ipc>;
describe('ImportNetwork component', () => {
const exportFilePath = join('/', 'tmp', 'polar', 'file', 'export.json');
@ -59,10 +59,11 @@ describe('ImportNetwork component', () => {
chart = initChartFromNetwork(network);
filesMock.read.mockResolvedValue(JSON.stringify({ network, chart }));
filesMock.rm.mockResolvedValue();
zipMock.unzip.mockResolvedValue();
fsMock.copy.mockResolvedValue(true as never);
osMock.tmpdir.mockReturnValue('/tmp');
osMock.platform.mockReturnValue('darwin');
const sender = jest.fn().mockResolvedValue(undefined);
ipcMock.createIpcSender.mockReturnValue(sender);
});
it('should display the file upload label', async () => {

12
src/components/network/NetworkView.spec.tsx

@ -6,6 +6,7 @@ import { fireEvent, wait, waitForElement } from '@testing-library/dom';
import { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Status } from 'shared/types';
import * as ipc from 'lib/ipc/ipcService';
import { initChartFromNetwork } from 'utils/chart';
import { defaultRepoState } from 'utils/constants';
import {
@ -16,14 +17,13 @@ import {
suppressConsoleErrors,
testCustomImages,
} from 'utils/tests';
import * as zip from 'utils/zip';
import NetworkView from './NetworkView';
jest.mock('utils/zip');
jest.mock('lib/ipc/ipcService');
const fsMock = fsExtra as jest.Mocked<typeof fsExtra>;
const logMock = log as jest.Mocked<typeof log>;
const zipMock = zip as jest.Mocked<typeof zip>;
const ipcMock = ipc as jest.Mocked<typeof ipc>;
const dialogMock = electron.remote.dialog as jest.Mocked<typeof electron.remote.dialog>;
const bitcoindServiceMock = injections.bitcoindService as jest.Mocked<
typeof injections.bitcoindService
@ -309,6 +309,11 @@ describe('NetworkView Component', () => {
});
describe('export network', () => {
beforeEach(() => {
const sender = jest.fn().mockResolvedValue(undefined);
ipcMock.createIpcSender.mockReturnValue(sender);
});
it('should fail to export a running network', async () => {
const { getByLabelText, findByText } = renderComponent('1', Status.Started);
fireEvent.mouseOver(getByLabelText('more'));
@ -339,7 +344,6 @@ describe('NetworkView Component', () => {
await wait(() => jest.runOnlyPendingTimers());
expect(logMock.info).toBeCalledWith('User aborted network export');
expect(dialogMock.showSaveDialog).toBeCalled();
expect(zipMock.zip).not.toBeCalled();
expect(queryByText("Exported 'test network'", { exact: false })).toBeNull();
jest.useRealTimers();
});

4
src/shared/ipcChannels.ts

@ -1,8 +1,10 @@
export default {
// general app chnnels
openWindow: 'open-Window',
openWindow: 'open-window',
clearCache: 'clear-cache',
http: 'http',
zip: 'zip',
unzip: 'unzip',
// LND proxy channels
getInfo: 'get-info',
walletBalance: 'wallet-balance',

5
src/store/models/network.spec.ts

@ -24,11 +24,6 @@ jest.mock('utils/files', () => ({
rm: jest.fn(),
}));
jest.mock('utils/zip', () => ({
zip: jest.fn(),
unzip: jest.fn(),
}));
const filesMock = files as jest.Mocked<typeof files>;
const logMock = log as jest.Mocked<typeof log>;
const detectPortMock = detectPort as jest.Mock;

10
src/utils/network.ts

@ -4,6 +4,7 @@ import { basename, join } from 'path';
import { IChart } from '@mrblenny/react-flow-chart';
import detectPort from 'detect-port';
import { tmpdir } from 'os';
import { ipcChannels } from 'shared';
import {
BitcoinNode,
CLightningNode,
@ -14,6 +15,7 @@ import {
NodeImplementation,
Status,
} from 'shared/types';
import { createIpcSender } from 'lib/ipc/ipcService';
import {
CustomImage,
DockerRepoImage,
@ -29,7 +31,6 @@ import { range } from './numbers';
import { isVersionCompatible } from './strings';
import { getPolarPlatform } from './system';
import { prefixTranslation } from './translate';
import { unzip, zip } from './zip';
const { l } = prefixTranslation('utils.network');
@ -588,7 +589,8 @@ export const importNetworkFromZip = async (
): Promise<[Network, IChart]> => {
// extract zip to a temp folder first
const tmpDir = join(tmpdir(), 'polar', basename(zipPath, '.zip'));
await unzip(zipPath, tmpDir);
const ipc = createIpcSender('NetworkUtil', 'app');
await ipc(ipcChannels.unzip, { filePath: zipPath, destination: tmpDir });
debug(`Extracted '${zipPath}' to '${tmpDir}'`);
// read and parse the export.json file
@ -666,5 +668,7 @@ export const zipNetwork = async (
const content = JSON.stringify({ network, chart });
await writeFile(join(network.path, 'export.json'), content);
// zip the network dir into the zip path
await zip(network.path, zipPath);
const ipc = createIpcSender('NetworkUtil', 'app');
await ipc(ipcChannels.zip, { source: network.path, destination: zipPath });
// await zip(network.path, zipPath);
};

131
src/utils/zip.spec.ts

@ -1,131 +0,0 @@
import fsExtra from 'fs-extra';
import { join } from 'path';
import * as archiver from 'archiver';
import { PassThrough } from 'stream';
import unzipper from 'unzipper';
import * as files from 'utils/files';
import { delay } from './async';
import { initChartFromNetwork } from './chart';
import { getNetwork } from './tests';
import { unzip, zip } from './zip';
jest.mock('utils/files');
const fsMock = fsExtra as jest.Mocked<typeof fsExtra>;
const filesMock = files as jest.Mocked<typeof files>;
const unzipperMock = unzipper as jest.Mocked<typeof unzipper>;
const archiverMock = archiver as jest.Mocked<any>;
describe('Zip Util', () => {
let unzipStream: PassThrough;
let zipStream: any;
let zippedPaths: string[];
const rpcPath = join('alice', 'lightningd', 'regtest', 'lightning-rpc');
beforeEach(() => {
const network = getNetwork();
const chart = initChartFromNetwork(network);
filesMock.read.mockResolvedValue(JSON.stringify({ network, chart }));
fsMock.pathExists.mockResolvedValue(true as never);
fsMock.mkdirp.mockResolvedValue(true as never);
fsMock.copy.mockResolvedValue(true as never);
fsMock.createWriteStream.mockReturnValue(new PassThrough() as any);
fsMock.createReadStream.mockImplementation(() => {
return {
pipe: jest.fn(() => {
// return the mock stream when "pipe()" is called
unzipStream = new PassThrough();
return unzipStream;
}),
} as any;
});
unzipperMock.Extract.mockImplementation(jest.fn());
archiverMock.mockImplementation(() => {
// return a fake stream when "archiver()" is called in the app
zipStream = new PassThrough();
zipStream.file = jest.fn();
zipStream.directory = jest.fn(
(src: string, dest: string, entryData: archiver.EntryDataFunction) => {
// call the entryData func on some test paths
const mockPaths = ['test.yml', 'other.json', rpcPath];
zippedPaths = [];
mockPaths.forEach(name => {
const result = entryData({ name });
if (result) zippedPaths.push(name);
});
},
);
zipStream.append = jest.fn();
zipStream.finalize = jest.fn();
// attach a func to emit events on the stream from the tests
zipStream.mockEmit = (event: any, data: any) => zipStream.emit(event, data);
return zipStream;
});
});
afterEach(() => {
if (unzipStream) unzipStream.destroy();
if (zipStream) zipStream.destroy();
});
describe('unzip', () => {
it('should unzip a file successfully', async () => {
const promise = unzip('source-path', 'destination-path');
// emit an error after a small delay
await delay(100);
unzipStream.emit('close');
await expect(promise).resolves.toBeUndefined();
});
it("fail to unzip something that isn't a zip", async () => {
const promise = unzip('source-path', 'destination-file');
// fail the unzip stream after a small delay
await delay(100);
unzipStream.emit('error', new Error('test-error'));
await expect(promise).rejects.toThrow();
});
it("fails to unzip something that doesn't exist", async () => {
fsMock.pathExists.mockResolvedValue(false as never);
return expect(unzip('source-path', 'destination-file')).rejects.toThrow();
});
});
describe('zip', () => {
it('should zip a folder successfully', async () => {
const promise = zip('source-path', 'destination-path');
// emit an error after a small delay
await delay(100);
zipStream.emit('finish');
await expect(promise).resolves.toBeUndefined();
});
it('should not include the c-lightning RPC in the zip', async () => {
const promise = zip('source-path', 'destination-path');
// emit an error after a small delay
await delay(100);
zipStream.emit('finish');
await expect(promise).resolves.toBeUndefined();
expect(zippedPaths).not.toContain(rpcPath);
expect(zippedPaths).toEqual(['test.yml', 'other.json']);
});
it('should fail if there is an archiver error', async () => {
const promise = zip('source-path', 'destination-path');
// emit an error after a small delay
await delay(100);
const mockError = new Error('test-error');
zipStream.emit('error', mockError);
await expect(promise).rejects.toEqual(mockError);
});
it('should fail if there is an archiver warning', async () => {
const promise = zip('source-path', 'destination-path');
// emit a warning after a small delay
const mockError = new Error('test-warning');
zipStream.emit('warning', mockError);
await expect(promise).rejects.toEqual(mockError);
});
});
});
Loading…
Cancel
Save