Skip to content

Commit 82a997f

Browse files
committed
initial commit
0 parents  commit 82a997f

File tree

6 files changed

+231
-0
lines changed

6 files changed

+231
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
npm-switch*/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# NPM Switch

index.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
const { app, Tray, Menu, BrowserWindow, shell, globalShortcut, Notification } = require('electron');
2+
const fs = require('fs').promises;
3+
const constants = require('fs').constants;
4+
const path = require('path');
5+
const os = require('os');
6+
const chokidar = require('chokidar');
7+
const debug = require('debug');
8+
const log = debug('npm-switch');
9+
10+
const icon = path.join(__dirname, 'npm_icon.png');
11+
const configDir = path.resolve(os.homedir(), '.npm_switch');
12+
const npmrcDir = path.resolve(configDir, '.npmrc');
13+
const configFile = path.join(configDir, 'config.json');
14+
const defaultNpmrc = path.resolve(os.homedir(), '.npmrc');
15+
16+
let tray;
17+
18+
// look into for a lighter alternative
19+
// https://github.com/zaaack/node-systray
20+
// https://github.com/mikaelbr/node-notifier
21+
22+
// https://nwjs.readthedocs.io/en/latest/References/Tray/
23+
24+
async function loadConfig() {
25+
let config;
26+
try {
27+
const buffer = await fs.readFile(path.join(configDir, 'config.json'));
28+
config = JSON.parse(buffer.toString());
29+
} catch {
30+
// no config
31+
config = {};
32+
}
33+
const files = await fs.readdir(npmrcDir);
34+
config.files = files;
35+
return config;
36+
}
37+
38+
async function getContextMenu(config) {
39+
globalShortcut.unregisterAll();
40+
const none = [];
41+
if (!config.selected) {
42+
none.push({
43+
enabled: false,
44+
label: '.npmrc is unmanaged',
45+
type: 'radio',
46+
checked: true
47+
});
48+
}
49+
const contextMenu = Menu.buildFromTemplate([
50+
...none,
51+
...config.files.map((label, i) => {
52+
const accelerator = `CommandOrControl+Shift+${i + 1}`;
53+
const action = async () => {
54+
if (config.selected === label) {
55+
return;
56+
}
57+
// select item;
58+
// pop up system notification "switched npm to..."
59+
await copyNpmrc(label);
60+
config.selected = label;
61+
await updateConfig(config);
62+
const notification = new Notification({
63+
title: 'NPM Switcher',
64+
icon,
65+
body: 'Switched .npmrc to ' + label,
66+
});
67+
notification.show();
68+
}
69+
globalShortcut.register(accelerator, action);
70+
return {
71+
label,
72+
accelerator,
73+
type: 'radio',
74+
click: action,
75+
checked: config.selected === label
76+
}
77+
}),
78+
{
79+
type: 'separator'
80+
},
81+
{
82+
label: 'Configure...',
83+
click: () => {
84+
shell.openPath(npmrcDir);
85+
}
86+
},
87+
{
88+
label: 'Close NPM switcher',
89+
type: 'normal',
90+
role: 'quit',
91+
click: () => {
92+
app.quit();
93+
}
94+
}
95+
]);
96+
return contextMenu;
97+
}
98+
99+
async function loadContextMenu(tray) {
100+
const config = await loadConfig();
101+
const contextMenu = await getContextMenu(config);
102+
tray.setContextMenu(contextMenu);
103+
log('config and menu loaded');
104+
return contextMenu;
105+
}
106+
107+
async function ensureDir(dir) {
108+
try {
109+
log('access ensureDir', dir);
110+
await fs.access(dir, constants.F_OK | constants.W_OK);
111+
} catch (err) {
112+
await fs.mkdir(dir);
113+
}
114+
}
115+
116+
async function ensureFile(file, defaultData) {
117+
try {
118+
log('access ensureFile', file);
119+
await fs.access(file, constants.F_OK | constants.W_OK);
120+
} catch (err) {
121+
await fs.writeFile(file, defaultData);
122+
}
123+
}
124+
125+
async function backupExisting() {
126+
const dir = path.resolve(configDir, '.npmrc.bak');
127+
log('backing up existing .npmrc to', dir);
128+
await fs.copyFile(defaultNpmrc, dir);
129+
}
130+
131+
async function ensureConfig() {
132+
await ensureDir(configDir);
133+
await ensureFile(configFile, JSON.stringify({
134+
target: defaultNpmrc
135+
}, null, '\t'));
136+
await ensureDir(npmrcDir);
137+
}
138+
139+
async function updateConfig({ selected, target }) {
140+
return fs.writeFile(configFile, JSON.stringify({
141+
selected,
142+
target
143+
}, null, '\t'));
144+
}
145+
146+
async function copyNpmrc(source) {
147+
try {
148+
log('checking access permissions', defaultNpmrc);
149+
await fs.access(defaultNpmrc, constants.F_OK | constants.W_OK);
150+
const stats = await fs.lstat(defaultNpmrc);
151+
if (!stats.isSymbolicLink()) {
152+
log('existing .npmrc is unmanaged, backing up...');
153+
await backupExisting();
154+
}
155+
log('removing existing file');
156+
await fs.unlink(defaultNpmrc);
157+
} catch (err) {
158+
console.log(err);
159+
}
160+
const newSource = path.resolve(npmrcDir, source);
161+
log('creating link to', newSource);
162+
await fs.symlink(newSource, defaultNpmrc, 'file');
163+
}
164+
165+
app.on('ready', async () => {
166+
log('app ready');
167+
try {
168+
await ensureConfig();
169+
win = new BrowserWindow({ show: false });
170+
tray = new Tray(icon);
171+
await loadContextMenu(tray);
172+
log('watching changes in', npmrcDir);
173+
const confWatcher = chokidar.watch(configFile);
174+
confWatcher.on('ready', () => {
175+
confWatcher.on('change', async (event, path) => {
176+
try {
177+
log('change detected in', configFile, event, path);
178+
await loadContextMenu(tray);
179+
} catch (err) {
180+
console.log(err);
181+
app.quit(1);
182+
}
183+
});
184+
});
185+
186+
const watcher = chokidar.watch(npmrcDir);
187+
watcher.on('ready', () => {
188+
watcher.on('all', async (event, path) => {
189+
if (event === 'change') {
190+
return;
191+
}
192+
try {
193+
log('change detected in', npmrcDir, event, path);
194+
await loadContextMenu(tray);
195+
} catch (err) {
196+
console.log(err);
197+
app.quit(1);
198+
}
199+
});
200+
});
201+
} catch (err) {
202+
console.log(err);
203+
app.quit(1);
204+
}
205+
});
206+

npm_icon.ico

279 KB
Binary file not shown.

npm_icon.png

794 Bytes
Loading

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "npm-switch",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"build": "electron-packager . npm-switch --overwrite --icon=npm_icon.png",
9+
"start": "electron ."
10+
},
11+
"author": "",
12+
"license": "ISC",
13+
"devDependencies": {
14+
"electron": "^9.0.5",
15+
"electron-packager": "^15.0.0"
16+
},
17+
"dependencies": {
18+
"debug": "^4.1.1",
19+
"chokidar": "^3.4.0"
20+
}
21+
}

0 commit comments

Comments
 (0)