Skip to content

Commit

Permalink
feat: add plugins support 🎉
Browse files Browse the repository at this point in the history
closes #14, #15, #16
  • Loading branch information
sbekrin committed Jul 3, 2018
1 parent e7801ce commit d2596c2
Show file tree
Hide file tree
Showing 26 changed files with 137,459 additions and 1,763 deletions.
20 changes: 9 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
language: node_js
node_js:
- 8
- 10
os:
- osx
osx_image: xcode9.2
matrix:
include:
- os: osx
osx_image: xcode9.3
language: node_js
node_js: 9
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
before_install:
- yarn global add greenkeeper-lockfile@1
install:
Expand All @@ -29,6 +30,3 @@ after_success:
- yarn release
after_failure:
- yarn create-diff-report
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"
17 changes: 9 additions & 8 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@
"dependencies": {
"electron-debug": "^2.0.0",
"electron-is-dev": "^0.3.0",
"electron-store": "^1.3.0",
"electron-updater": "^2.21.10",
"electron-util": "^0.8.2",
"electron-store": "^2.0.0",
"electron-unhandled": "^1.1.0",
"electron-util": "^0.9.0",
"execa": "^0.10.0",
"fix-path": "^2.1.0",
"fuse.js": "^3.2.0",
"fuse.js": "^3.2.1",
"lodash.debounce": "^4.0.8",
"lodash.flatten": "^4.4.0",
"menubar": "^5.2.3",
"polished": "^1.9.2",
"polished": "^1.9.3",
"string-to-color": "^2.0.0",
"styled-components": "^3.3.0",
"thenby": "^1.2.3",
"styled-components": "^3.3.3",
"thenby": "^1.2.6",
"tree-kill": "^1.2.0",
"unstated": "^2.1.1"
"unstated": "^2.1.1",
"update-electron-app": "^1.3.0"
}
}
Empty file removed app/src/about-window/.gitkeep
Empty file.
15 changes: 8 additions & 7 deletions app/src/common/app-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Container } from 'unstated';
import debounce from 'lodash.debounce';
import Fuse from 'fuse.js';
import formatPath from '~/common/format-path';
import createStore from '~/common/preferences-store';
import Channels from '~/common/channels';
import preferences from '~/preferences';

const FAILED_DEBOUNCE_WAIT = 500;
const DEFAULT_STATE = {
Expand All @@ -26,7 +26,9 @@ export default class AppState extends Container {

searchInputRef = null;

preferences = createStore();
preferences = preferences;

projectPluginActions = [];

constructor(...args) {
super(...args);
Expand Down Expand Up @@ -88,10 +90,13 @@ export default class AppState extends Container {
: projects.sort(sortFn);
}

refreshProjects() {
refreshPreferences() {
// Refresh projects
this.state.projects.forEach(project => {
ipcRenderer.send(Channels.PROJECT_OPEN_REQUEST, project.path);
});
// Refresh plugins
ipcRenderer.send(Channels.PLUGINS_LOAD);
}

setSelected(project) {
Expand Down Expand Up @@ -197,8 +202,4 @@ export default class AppState extends Container {
ipcRenderer.send(Channels.PROJECT_OPEN_REQUEST, file.path)
);
}

checkForUpdates() {
ipcRenderer.send(Channels.CHECK_FOR_UPDATE);
}
}
2 changes: 1 addition & 1 deletion app/src/common/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export default {
SCRIPT_STOP: 'script-stop',
SCRIPT_EXITED: 'script-exited',
CHECK_FOR_UPDATE: 'update-check',
CHECK_FOR_UPDATE_RESULT: 'update-check-result',
PLUGINS_LOAD: 'plugins-load',
};
72 changes: 45 additions & 27 deletions app/src/index.main.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
import '~/setup.main';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import util from 'util';
import execa from 'execa';
import { app, ipcMain, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import checkForUpdates from 'update-electron-app';
import electronUtil from 'electron-util';
import electronDebug from 'electron-debug';
import unhandled from 'electron-unhandled';
import createMenubar from 'menubar';
import invariant from 'invariant';
import stringToColor from 'string-to-color';
import treeKill from 'tree-kill';
import fixPath from 'fix-path';
import createNotification from '~/common/notification';
import createStore from '~/common/preferences-store';
import Channels from '~/common/channels';
import Constants from '~/common/constants';
import plugins from '~/plugins';
import preferences from '~/preferences';
import menubarIcon from '~/assets/menubarTemplate.png';
import '~/assets/menubarTemplate@2x.png';

// Setup test-related stuff until more elegant soultion is found
if (global.process.env.NODE_ENV === 'test') {
// Point userData to temp directory
app.setPath('userData', app.getPath('temp'));
// Make sure visual snapshots are the same on CI
app.commandLine.appendSwitch('high-dpi-support', 'true');
app.commandLine.appendSwitch('force-device-scale-factor', '2');
}
const APP_NAME = 'npmkit';
const UPDATE_INTERVAL = '1 hour';

if (electronUtil.is.development) {
electronDebug({ showDevTools: false });
} else {
fixPath();
unhandled();
checkForUpdates({ updateInterval: UPDATE_INTERVAL });
}

const APP_NAME = 'npmkit';
const isDev = process.env.NODE_ENV === 'development';
const readFileAsync = util.promisify(fs.readFile);
const statAsync = util.promisify(fs.stat);
const treeKillAsync = util.promisify(treeKill);
const preferences = createStore().ensureDefaults();
const menubar = createMenubar({
index: isDev ? 'http://localhost:8080' : `file://${__dirname}/index.html`,
width: 320,
Expand All @@ -55,7 +51,7 @@ const menubar = createMenubar({

function showTray() {
menubar.positioner.move('trayCenter', menubar.tray.getBounds());
menubar.window.show();
menubar.showWindow();
}

async function getProjectData(projectPath) {
Expand Down Expand Up @@ -93,25 +89,13 @@ async function getProjectData(projectPath) {
};
}

async function checkForUpdates() {
return await autoUpdater.checkForUpdatesAndNotify();
}

menubar.on('after-create-window', () => {
menubar.tray.on('drag-enter', showTray);
});

app.on('ready', async () => {
menubar.window.on('ready-to-show', showTray);
await electronUtil.enforceMacOSAppLocation();
await checkForUpdates();
// Check for updates periodically
setInterval(checkForUpdates, Constants.UPDATE_CHECK_INTERVAL);
});

// Request to check for an app update
ipcMain.on(Channels.CHECK_FOR_UPDATE, async event => {
event.sender.send(Channels.CHECK_FOR_UPDATE_RESULT, await checkForUpdates());
});

// Open all projects (e.g. on first run)
Expand Down Expand Up @@ -217,6 +201,40 @@ ipcMain.on(Channels.SCRIPT_RUN, (event, { project, script }) => {
});

// Stops running process
ipcMain.on(Channels.SCRIPT_STOP, async (event, { pid }) => {
ipcMain.on(Channels.SCRIPT_STOP, async (_, { pid }) => {
await treeKillAsync(pid);
});

// Load plugins
ipcMain.on(Channels.PLUGINS_LOAD, async () => {
const failed = [];
const result = await Promise.all(
preferences.get('plugins').map(async plugin => {
try {
const pluginMod = await plugins.load(plugin);
if (pluginMod.onLoad) {
pluginMod.onLoad();
}
} catch (reason) {
failed.push([plugin, reason]);
}
})
);
if (result.length) {
console.log(`Loaded ${result.length} plugins`);
createNotification('Plugins', `Loaded ${result.length} plugins`);
}
for (const [plugin, reason] of failed) {
console.error('Failed to load plugin:', reason);
createNotification(
APP_NAME,
`Failed to load plugin ${plugin}, click for details`
).on('click', () => {
const errors = reason.toString().match(/^error (.+)$/gm);
dialog.showErrorBox(
APP_NAME,
`Failed to load ${plugin}:\n\n${errors.join('\n')}`
);
});
}
});
1 change: 1 addition & 0 deletions app/src/plugins/available.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default new Set(['onLoad', 'onUnload', 'getProjectMenu']);
9 changes: 9 additions & 0 deletions app/src/plugins/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { join, dirname } from 'path';
import preferences from '~/preferences';

const PLUGINS_DIR = '.npmkit_plugins';

export default function getPluginsDir(mod) {
const base = join(dirname(preferences.path), PLUGINS_DIR);
return mod ? join(base, 'node_modules', mod) : base;
}
10 changes: 10 additions & 0 deletions app/src/plugins/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import preferences from '~/preferences';
import base from './base';

export default function getPluginHooks(name) {
return preferences
.get('plugins')
.map(name => __non_webpack_require__(base(name)))
.map(plugin => (plugin.hasOwnProperty(name) ? plugin[name] : false))
.filter(Boolean);
}
13 changes: 13 additions & 0 deletions app/src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import available from './available';
import install from './install';
import load from './load';
import hooks from './hooks';
import base from './base';

export default {
available,
load,
install,
hooks,
base,
};
45 changes: 45 additions & 0 deletions app/src/plugins/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import cp from 'child_process';
import util from 'util';
import { join, resolve } from 'path';
import { ensureDir, writeJson } from 'fs-extra';
import preferences from '~/preferences';
import getPluginsDir from './base';

const execFileAsync = util.promisify(cp.execFile);
const yarn = resolve(__dirname, '../../bin/yarn-standalone.js');

async function install(pluginName) {
const pluginsDir = getPluginsDir();
const cacheDir = join(pluginsDir, 'cache');
await ensureDir(pluginsDir);
await ensureDir(cacheDir);
await writeJson(join(pluginsDir, 'package.json'), {
name: 'npmkit-plugins',
description: 'Auto-generated by npmkit',
private: true,
version: '0.0.1',
dependencies: preferences
.get('plugins')
.reduce((deps, name) => ({ ...deps, [name]: 'latest' }), {}),
});
await execFileAsync(
process.execPath,
[
yarn,
'install',
'--no-emoji',
'--no-lockfile',
'--cache-folder',
cacheDir,
],
{
cwd: pluginsDir,
env: { NODE_ENV: 'production', ELECTRON_RUN_AS_NODE: 'true' },
timeout: 1000 * 60 * 5,
maxBuffer: 1024 * 1024,
}
);
return __non_webpack_require__(getPluginsDir(pluginName));
}

export default install;
16 changes: 16 additions & 0 deletions app/src/plugins/load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import install from './install';
import getPluginsDir from './base';

export default async function loadPlugin(pluginName) {
let mod;
try {
mod = __non_webpack_require__(getPluginsDir(pluginName));
} catch (reason) {
if (/Cannot find module/.test(reason.toString())) {
mod = await install(pluginName);
} else {
throw reason;
}
}
return mod.default ? mod.default : mod;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class PreferencesStore extends Store {
this.set('editor', this.get('editor', this.getDefaultEditor()));
this.set('pinned', this.get('pinned', []));
this.set('projects', this.get('projects', []));
this.set('plugins', this.get('plugins', []));
return this;
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/preferences/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import createStore from './create-store';

const preferences = createStore().ensureDefaults();

export default preferences;
Empty file.
9 changes: 9 additions & 0 deletions app/src/setup.main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { app } from 'electron';

if (global.process.env.NODE_ENV === 'test') {
// Point userData to temp directory
app.setPath('userData', app.getPath('temp'));
// Make sure visual snapshots are the same on CI
app.commandLine.appendSwitch('high-dpi-support', 'true');
app.commandLine.appendSwitch('force-device-scale-factor', '2');
}
Loading

0 comments on commit d2596c2

Please sign in to comment.