1 (изменено: dab00, 2015-02-07 19:20:19)

Тема: JS: менеджер версий Node.js

Всем привет. Разрешите представить: Node Version Manager на WSH.
Не уверен что лучшим образом подошел к решению вопроса... в общем кому интересно - добро пожаловать: ваши замечания, pull request'ы и просто комменты.

nvmx.js:



var args = (function() {
  var args = WScript.Arguments, arr = [];
  for (var i = 0, len = args.length; i < len; i += 1) {
    arr[i] = trimLeft(args.item(i));
  }
  return arr;

  function trimLeft(item) {
    var str = trimFirst(item), substr = str.substring(0,2);
    return indexOf(substr) ? substr : str;
  }

  function trimFirst(item) {
    var s = item.substring(0,1);
    if (s === '/') return item.substr(1);
    if (s === '-') return item.substring(1,2) === '-' ? item.substr(2) : item.substr(1);
    return item;
  }

  function indexOf(s) {
    var arr = ['no', 'io', 'lt', 'la', 'a', 'al', 'i', 'in', 'ls', 'li', 'rm', 're', 'ui', 'un', 'u', 'us', 'h', 'he', '?'], i = arr.length;
    while (i--) {
      if (arr[i] === s) return true;
    }
  }
})();

(function() {
  switch(args[0]) {
  case 'h':
  case 'he':
  case '?':
    return help();
  case 'no':
  case 'io':
    return go(args[1], args[0]); // Wrong sequence of arguments
  default:
    return go(args[0], args[1]); // Wright sequence of arguments
  }
})();

function go(command, arg) { // arg = platform || version || architecture || undefined
  switch(command) {
  case 'lt':
  case 'la':
    latest(arg);
  case 'a':
  case 'al':
    all(arg);
  case 'i':
  case 'in':
    install(arg);
  case 'ls':
  case 'li':
    list(arg);
  case 'u':
  case 'us':
    use(arg);
  case 'rm':
  case 're':
  case 'ui':
  case 'un':
    remove(arg);
  case 'off':
    off();
  case 'path':
    path();
  default:
    last();
  }
}

function platform(arg) {
  var config = {
    no: {
      distUrl: 'http://nodejs.org/dist/',
      latestUrl: 'http://nodejs.org/dist/latest',
      name: 'node',
      folder: 'nodejs',
      exe: 'node.exe',
      getUrl: function(name, version) {
        var url = this.distUrl + version, arch = getArch();
        if (arch === 'x64') url += '/x64';
        return url + '/' + name + '.msi';
      }
    },
    io: {
      distUrl: 'https://iojs.org/dist/',
      latestUrl: 'https://iojs.org/dist/latest',
      name: 'iojs',
      folder: 'iojs',
      exe: 'iojs.exe',
      getUrl: function(name, version) {
        return this.distUrl + version + '/' + name + '.msi';
      }
    }
  };
  return config[ arg in config ? arg : 'no' ]; // default = node
}

//-----
function latest(arg) {
  var obj = platform(arg), version = getLatest(obj.latestUrl);
  if (!version) return err('Failed getting latest ' + obj.name + ' version');
  log('Latest ' + obj.name + ' version: ' + version);
  return quit(0);
}

function getLatest(path) { // latest version
  var ie = getIE(path);
  var elements = ie.document.getElementsByTagName('a'), i = elements.length;
  var arr, text, ret;
  if (i) {
    while (i--) {
      text = elements[i].innerText;
      if (!text) continue;
      arr = text.split('-');
      if (isVersion(arr)) {
        ie.quit();
        return arr[1].split('.').splice(0,3).join('.');
      }
    }
  }
}

function isVersion(arr) {
  return arr[1] && arr[1].substring(0,1) && arr[1].substring(0,1).toLowerCase() === 'v';
}

function getIE(path) {
  var ie = ie_(), i = 0, timeout = 200;
  while (!ie) {
    if (timeout > 5000) return err('Failed getting InternetExplorer.Application. Please try again');
    if (timeout%1000 === 0) log('Please wait...');
    WScript.Sleep(timeout += 200);
    ie = ie_();
  }
  ie.visible = false;
  ie.navigate(path);
  while (ie.busy || ie.readyState !== 4) {
    if (++i > 300) return err('Failed getting response from ' + path);
    WScript.Sleep(200);
  }
  return ie;

  function ie_() {
    try {
      return WScript.CreateObject('InternetExplorer.Application');
    } catch(e) {
      // log(e.message && e.message.toString());
    }
  }
}
//-----

//-----
function all(arg) { // all versions
  var obj = platform(arg);
  var ie = getIE(obj.distUrl);
  var elements = ie.document.getElementsByTagName('a'), i = elements.length;
  var arr, text;
  if (i) {
    log('Available ' + obj.name + ' versions:');
    while (i--) {
      text = elements[i].innerText;
      if (!text) continue;
      if (text.indexOf('-') !== -1) continue;
      arr = text.split('.');
      if (arr.length < 3 || arr[1] === '') continue;
      if (obj.name === 'node') {
        if (7 < arr[1]) log(text.substring(0, text.length - 1));
      } else {
        log(text.substring(0, text.length - 1));
      }
    }
  } else {
    return err('Failed getting ' + obj.name + ' available versions');
  }
  ie.quit();
  return quit(0);
}
//-----

//-----
function install(arg) {
  var obj = platform(arg);
  var param = (function() {
    var version = fixArg(arg);
    if (!version || version === 'latest') return getLatest(obj.latestUrl); // default
    return fixVersion(version);
  })();
  var dirName = obj.name + '-' + param + '-' + getArch();
  log('Installing ' + dirName);
  var remote = obj.getUrl(dirName, param);
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var local = fso.GetSpecialFolder(2) + '\\' + dirName, localmsi = local + '.msi';
  log('Downloading...');
  // log('Downloading \nfrom: ' + remote + '\nto:' + localmsi);
  download(remote, localmsi);
  log('Extracting...');
  // log('Extracting to: ' + local);
  WScript.CreateObject('WScript.Shell').Run('msiexec /a '  + localmsi + ' /qn TARGETDIR=' + local, 0, true);
  var copyFrom = local + '\\' + obj.folder, copyTo = getCurr(fso) + '\\' + dirName;
  log('Copying...');
  // log('Copying \nfrom: ' + copyFrom + '\nto: ' + copyTo);
  if (!fso.folderExists(copyFrom)) err('Folder ' + copyFrom + ' not found');
  fso.copyFolder(copyFrom, copyTo);
  var binPath = copyTo + '\\' + 'node.exe';
  if (!fso.fileExists(binPath)) fso.copyFile(copyTo + '\\' + obj.exe, binPath);
  log('Installed ' + dirName);
  return quit(0);

  function download(remote, local) {
    var xhr = WScript.CreateObject('MSXML2.XMLHTTP');
    xhr.open('GET', remote, 0);
    xhr.send();
    if (xhr.readyState !== 4) return err('Failed to download from ' + remote);
    if (xhr.status !== 200) return err('Failed to download from ' + remote);
    var stream = WScript.CreateObject('ADODB.Stream');
    stream.mode = 3;
    stream.type = 1;
    stream.open()
    stream.write(xhr.responseBody);
    stream.saveToFile(local, 2);
  }
}
//-----

//-----
function list(arg) {
  var platforms = arg ? [platform(arg)] : [platform('no'), platform('io')];
  var bins = [], names = [], i = platforms.length, arch = getArch();
  while (i--) {
    bins[i] = platforms[i].exe;
    names[i] = platforms[i].name;
  }
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var folders = new Enumerator(fso.GetFolder(getCurr(fso)).subFolders);
  var files;
  for (; !folders.atEnd(); folders.moveNext()) {
    if (!isTargetFolder(folders.item().Name, arch)) continue;
    files = new Enumerator(folders.item().Files);
    for (; !files.atEnd(); files.moveNext()) {
      if (isTargetBin(files.item().Name)) {
        log(folders.item().Name)
        break;
      }
    }
  }
  return quit(0);

  function isTargetFolder(folderName, arch) {
    var i = names.length, arr = folderName.split('-');
    while (i--) {
      if (names[i] === arr[0] && arch === arr[2]) return true;
    }
  }

  function isTargetBin(fileName) {
    var i = bins.length;
    while (i--) {
      if (bins[i] === fileName) return true;
    }
  }
}
//-----

//-----
function use(arg) {
  if (!arg) return last(); // last used version
  var arr = getPath();
  var version = fixVersion(fixArg(arg));
  var dirName = platform(arg).name + '-' + version + '-' + getArch();
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var current = getCurr(fso);
  var folders = new Enumerator(fso.GetFolder(current).subFolders);
  for (; !folders.atEnd(); folders.moveNext()) {
    if (folders.item().Name === dirName) {
      var file = fso.CreateTextFile(current + '\\' + 'path.txt', true);
      file.write((function() {
        var first = arr[0].split('\\');
        if (first.slice(0, first.length - 1).join('\\') === current) arr.shift();
        return current + '\\' + dirName + ';' + arr.join(';');
      })());
      file.close();
      log('Now using ' + dirName);
      return quit(9);
    }
  }
  err('Version ' + dirName + ' not found');
}

function getPath() {
  var path = WScript.CreateObject('WScript.Shell').ExpandEnvironmentStrings('%PATH%');
  return !(path && path.length) ? err('Invalid %PATH%') : path.split(';');
}
//-----

//-----
function remove(arg) {
  if (!arg) err('Invalid version');
  var version = fixVersion(fixArg(arg));
  var dirName = platform(arg).name + '-' + version + '-' + getArch();
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var folders = new Enumerator(fso.GetFolder(getCurr(fso)).subFolders);
  for (; !folders.atEnd(); folders.moveNext()) {
    if (folders.item().Name === dirName) {
      fso.deleteFolder(folders.item().Path, true);
      log('Removed ' + dirName);
      return quit(0);
    }
  }
  err('Version ' + dirName + ' not found');
}
//-----

//-----
function last() { // last used version
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var current = getCurr(fso), path = current + '\\' + 'path.txt';
  if (!fso.fileExists(path)) return help();
  var file = fso.OpenTextFile(path, 1), arr = file.readAll().split(';');
  file.close();
  if (arr.length === 0) return err('Invalid path.txt');
  var first = arr[0].split('\\'), version = first.pop();
  if (first.join('\\') !== current) return log('Last used version not found');
  if (isVersion(version.split('-'))) {
    log('Now using ' + version);
    return quit(9);
  }
}
//-----

//-----
function off() {
  var arr = getPath();
  var fso = WScript.CreateObject('Scripting.FileSystemObject');
  var current = getCurr(fso);
  var first = arr[0].split('\\');
  log('Now nvmx is off');
  if (first.slice(0, first.length - 1).join('\\') === current) {
    var file = fso.CreateTextFile(current + '\\' + 'path.txt', true);
    file.write(arr.slice(1).join(';'));
    file.close();
    return quit(9);
  }
  return quit(0);
}
//-----

//-----
function path() {
  log(getCurr(WScript.CreateObject('Scripting.FileSystemObject')));
  quit(0);
}
//-----

function fixVersion(version) {
  var arr = version.split('.');
  if (arr.length !== 3 || arr[2].length === 0 || arr[1].length === 0 || arr[0].length === 0) return err('Invalid version');
  if (version.substring(0,1).toLowerCase() === 'v') return version;
  return 'v' + version;
}

function fixArg(arg) {
  return arg === 'io' ||  arg === 'no' ? args[2] : arg; // may be blank
}

function getArch() { // architecture
  var a = args[args.length - 1]; // last arg
  if (a.substring(0,1).toLowerCase() === 'x') a = a.substr(1);
  if (a === '64' || a === '86') return 'x' + a;
  return WScript.CreateObject('WScript.Shell').Environment('System')('PROCESSOR_ARCHITECTURE') === 'x86' ? 'x86' : 'x64';
}

function getCurr(fso) {
  return fso.GetParentFolderName(fso.GetFile(WScript.ScriptFullName));
}

function help() {
  var version = 'v0.1.1', desc = 'Lightweight Node Version Manager for Windows';
  log(desc + ' '  + version + '\n' +
      'Commands:' + '\n' +
      '  nvmx latest    [platform]                          Output latest available version' + '\n' +
      '  nvmx all       [platform]                          Output all available versions' + '\n' +
      '  nvmx install   [platform] <version> [architecture] Download and extract (install) <version>' + '\n' +
      '  nvmx list      [platform]           [architecture] Output local versions' + '\n' +
      '  nvmx use       [platform] <version> [architecture] Modify PATH to use <version>' + '\n' +
      '  nvmx remove    [platform] <version> [architecture] Remove (uninstall) <version>' + '\n' +
      '  nvmx uninstall [platform] <version> [architecture] Same as remove' + '\n' +
      '  nvmx ?                                             Output help message' + '\n' +
      '  nvmx                                               Modify PATH to use last used version' + '\n' +
      '  nvmx off                                           Modify PATH to not use nvmx' + '\n' +
      '  nvmx path                                          Output path to nvmx folder' + '\n' +
      'Aliases:' + '\n' +
      '  lt latest    -lt --latest    /lt /latest' + '\n' +
      '  a  all       -a  --all       /a  /all' + '\n' +
      '  i  install   -i  --install   /i  /install' + '\n' +
      '  ls list      -ls --list      /ls /list' + '\n' +
      '  u  use       -u  --use       /u  /use' + '\n' +
      '  rm remove    -rm --remove    /rm /remove' + '\n' +
      '  ui uninstall -ui --uninstall /ui /uninstall' + '\n' +
      '  h  help      -h  --help      /h  /help      ? /?' + '\n' +
      'Platforms:' + '\n' +
      '  node nodejs node.js' + '\n' +
      '  io   iojs   io.js' + '\n' +
      'Architectures:' + '\n' +
      '  x64 64' + '\n' +
      '  x86 86' + '\n' +
      'Defaults:' + '\n' +
      '  platform     = node' + '\n' +
      '  architecture = os architecture');
}

function quit(code) {
  WScript.Quit(code);
}

function log(s) {
  WScript.Echo(s);
}

function err(s, code) {
  log(s);
  quit(code || 0);
}

nvmx.bat:


@echo off
cscript.exe //nologo "%~dp0nvmx.js" %*
if %errorlevel% == 9 (
  call :use
  exit /b
)
exit /b
:use
set /p nvmx=<%~dp0path.txt
set "path=%nvmx%"

Кстати, возвращаясь к одной из предыдущих тем форума: здесь я использую тот же код для определения разрядности оси.

2 (изменено: Rumata, 2015-02-01 23:37:25)

Re: JS: менеджер версий Node.js

Первоначальная моя мысль была пройтись с целью оптимизации некоторых функций (например, trimLeft, trimFirst, indexOf), но по мере знакомства со скриптом намерения немного изменились .

Мне кажется поддерживать такое большое количество вариантов аргументов излишне (слово, /слово, --слолво, /с, -с, с). Пусть это будут полные имена (latest, all, install и т.д.) и некоторые короткие варианты (ls, rm). Например, "l" (строчная латинская "эль") интуитивнее как сокрашение для list, но не для latest.

Install и Remove это взаимно противоположные действия? Тогда - Install и Uninstall.

Такое большое количество вариантов для справки (help, --help, /help, -h, /h. /?, ?) подавляет: запуск без параметров уже выводит справку.

Только первый аргумент является указателем действия, остальные - это параметры этого действия. Но "откусывание" ведущих слеша, тире и двойного тире производится для для всех. То есть я могу написать nvmx /list /node. Наверно, это не совсем так. Да и в справке не отражено такое поведение.

Обработку аргументов командной строки можно существенно сократить. Например, так:


var actionList = {
    'latest': latest, 
    'all': all, 

    'install': install, 
    'uninstall': uninstall, 

    'list': list, 
    'ls': list, 

    'use': use, 

    'help': help
};

var action = args.shift();

actionList[action](args) || help();
( 2 * b ) || ! ( 2 * b )

3

Re: JS: менеджер версий Node.js

Кстати, возвращаясь к одной из предыдущих тем форума: здесь я использую тот же код для определения разрядности оси.

Вот и будете иметь проблему на Winx64, запуская скрипт из среды в режиме эмуляции x32.

4

Re: JS: менеджер версий Node.js

Спасибо за участие.

По оптимизации: если про то чтобы вытащить trimFirst и indexOf из trimLeft - выше цикл все-таки, то я не думаю что здесь это имеет смысл, если бы там был какой-нибудь мегацикл... а здесь как рука взяла так и осталось , да и вряд ли это особенно оптимизирует, в общем по-моему на любителя, если я конечно правильно догадался что подразумевалось под оптимизацией.

По аргументам: здесь я конечно немного перегнул, скрипт задумывался с претензией на универсальность и на мой взгляд "интуитивность" у каждого своя, вот я и навтыкал алиасов на сколько фантазии хватило. Кроме того кому-то интуитивнее написать nvmx install io, а кому-то nvmx io install, поэтому постарался предусмотреть и этот вариант.
Дальше. Мне не хотелось "мистифицировать" это приложение и сделать его работу максимально "прозрачной", Install и Remove я бы не назвал противоположными понятиями: Install - качает версию, распаковывает, после чего копирует распакованный каталог из темпового каталога юзера в каталог приложения, Remove - просто удаляет каталог из каталога приложения, я бы не назвал это Uninstall... хотя как вариант наверное имеет смысл добавить Uninstall в список алиасов, да и у "альтернатив" это называется uninstall.
Но удалять многочисленные алиасы не буду, мне например удобнее написать nvmx rm ... чем nvmx uninstall ... , короче же

На счет "откусывания": думаю неважно как написать, главное чтобы сработало, хотя в справке действительно вариант типа nvmx /list /node не отражен.

Ну и по разрядности оси: по-моему вероятность необходимости использования этого решения в условиях "эмуляций" и т.п. близка к нулю.

И еще. Если не считать таких мелочей как отсутствие indexOf или неверная работа substr с отрицательным аргументом позиции мне больше всего хлопот доставил InternetExplorer.Application и поиск вариантов вернуть строку из скрипта в батник, ну и для xhr наверное можно было что-то "покруче" чем MSXML2.XMLHTTP применить, к примеру чтобы можно было прокси указать. Откровенно говоря думал что по этим вопросам какие-нибудь замечания будут.

Спасибо еще раз, все услышал.

5

Re: JS: менеджер версий Node.js

dab00 пишет:

Ну и по разрядности оси: по-моему вероятность необходимости использования этого решения в условиях "эмуляций" и т.п. близка к нулю.

Глубоко ошибаетесь. Продвинутые пользователи, коевы и будут данное детище потенциально рассматривать для своих нужд, вместо проводника предпочитают использовать совсем другие оболочки, и чаще именно 32-битные.

6

Re: JS: менеджер версий Node.js

Flasher пишет:

Глубоко ошибаетесь

Потенциальные пользователи приложения - Node.js девелоперы, которым приходится работать в Windows, думаю что у таковых нет необходимости эмулировать сколько-нибудь-битную Windows, скорее Red Hat какой-нибудь, типа CentOS, а "продвинутым" это ни к чему

7

Re: JS: менеджер версий Node.js

думаю что у таковых нет необходимости эмулировать сколько-нибудь-битную Windows

Никто не пишет про эмуляцию операционной системы (к чему она?).  Речь идёт о 32-х битных приложениях, из которых будет запускаться скрипт.

8

Re: JS: менеджер версий Node.js

Отредактировал код исходного поста, ну и версию приложения:
- изменил код определения разрядности оси согласно рекомендации Flasher
- добавил аргумент uninstall благодаря замечанию Rumata
- плюс еще несколько несущественных улучшений
Надеюсь что окончательно победил ошибку "Не удается создать объект с именем "InternetExplorer.Application"". Решил следующим образом:


function getIE(path) {
  var ie = ie_(), i = 0, timeout = 200;
  while (!ie) {
    if (timeout > 5000) return err('Failed getting InternetExplorer.Application. Please try again');
    if (timeout%1000 === 0) log('Please wait...');
    WScript.Sleep(timeout += 200);
    ie = ie_();
  }
  ie.visible = false;
  ie.navigate(path);
  while (ie.busy || ie.readyState !== 4) {
    if (++i > 300) return err('Failed getting response from ' + path);
    WScript.Sleep(200);
  }
  return ie;

  function ie_() {
    try {
      return WScript.CreateObject('InternetExplorer.Application');
    } catch(e) {
      // log(e.message && e.message.toString());
    }
  }
}

Наверное есть решение лучше... но пока вот как-то так .
Спасибо всем кто не прошел мимо.