Публикация реального пакета NodeJS: реализация и тесты

Для одного из моих проектов TypeScript мне понадобился инструмент для распаковки, совместимый с Windows. Я не нашел ни одного, в котором есть файл декларации d.ts. Поэтому я решил реализовать пакет NodeJs zip/unzip со встроенными объявлениями TypeScript.

Конечно, было бы проще написать файл декларации для одного из существующих инструментов, но пошаговое руководство по публикации пакета NodeJS было бы полезнее.

Из-за длины темы я разделил руководство на четыре статьи:



Публикация реального пакета NodeJS: Project Bootstrap
Пошаговое руководство по публикации пакета NodeJS.medium.com



  • Реализация и тестирование (текущая версия)
  • Улучшения качества кода (ожидается)
  • Публикация с помощью GitHub Actions (ожидается)

Выполнение

В конце предыдущей статьи мы опубликовали версию 0.0.1 нашего нового пакета NodeJS на npmjs.com. В этой статье мы собираемся реализовать и протестировать функции zipSync и unzipSync.

Для реализации будем использовать:

  • Командлет Windows Compress-Archive: создает сжатый архив из указанных файлов и каталогов,
  • Командлет Windows Expand-Archive: извлекает файлы из указанного файла архива,
  • и функция Node.child_process execFileSync, которая может запускать исполняемые файлы с параметрами.

Реализация функции zipSync

Функция zipSync будет оболочкой для командлета Compress-Archive. Мы будем использовать параметры командлета -Path, -DestinationPath и -Force, но будем передавать значения только для -Path и -DestinationPath с помощью функции zipSync:

function zipSync(path, dest) {
}

Аргумент path может быть строкой или массивом строк, поэтому мы должны преобразовать входную строку в массив с одним элементом:

if (!Array.isArray(path)) {
  path = [path];
}

В следующих нескольких строках execFileSync запустит командлет Compress-Archive и его параметры в powershell.exe:

execFileSync(
  'powershell.exe',
  [
    'Compress-Archive',
    '-Path', path.map(p => `"${p}"`).join(', '),
    '-DestinationPath', `"${dest}"`,
    '-Force'
  ],
);

Вот полный zip.js:

const { execFileSync } = require('child_process');
function zipSync(path, dest) {
  if (!Array.isArray(path)) {
    path = [path];
  }
  execFileSync(
    'powershell.exe',
    [
      'Compress-Archive',
      '-Path', path.map(p => `"${p}"`).join(', '),
      '-DestinationPath', `"${dest}"`,
      '-Force'
    ],
    {
      maxBuffer: Infinity,
      windowsHide: true
    }
  );
}
module.exports = {
  zipSync
};

Реализация функции unzipSync

Функция unzipSycn будет оболочкой для командлета Expand-Archive. Реализация почти такая же, как zipSync. Мы будем использовать параметры командлета -Path, -DestinationPath и -Force, но мы передавать значения для -Path и -DestinationPath через функцию unzipSync.

function unzipSync(path, dest) {
}

Но в этом случае нам не нужно обрабатывать несколько путей, поэтому нам просто нужно запустить командлет Expand-Archive следующим образом:

execFileSync(
  'powershell.exe',
  [
    'Expand-Archive',
    '-Path', `"${path}"`,
    '-DestinationPath', `"${dest}"`,
    '-Force'
  ],
  {
    maxBuffer: Infinity,
    windowsHide: true
  }
);

Вот полный unzip.js:

const { execFileSync } = require('child_process');
function unzipSync(path, dest) {
  execFileSync(
   'powershell.exe',
   [
     'Expand-Archive',
     '-Path', `"${path}"`,
     '-DestinationPath', `"${dest}"`,
     '-Force'
   ],
   {
     maxBuffer: Infinity,
     windowsHide: true
   }
  );
}
module.exports = {
  unzipSync
};

Обновление index.d.ts

Полезно добавить комментарии к файлу d.ts. Это будет полезно для других разработчиков, использующих наш пакет.

Вот полный index.d.ts:

/**
* Creates a compressed archive, or zipped file, from specified files and directories.
* This is a wrapper function for Compress-Archive.
* @param {string|string[]} path Specifies the path or paths to the files to add to the archive zipped file.
* @param {string} dest Specifies the path to the archive output file.
*/
export declare function zipSync(path: string|string[], dest: string): void;
/**
* Extracts files from a specified achive (zipped) file.
* This is a wrapper function for Expand-Archive.
* @param {string} path Specifies the path to the archive file.
* @param {string} dest Specifies the path to the output folder.
*/
export declare function unzipSync(path: string, dest: string): void;

Тестирование

Прежде чем опубликовать реализованную версию нашего пакета, мы должны убедиться, что он работает должным образом. Нам нужно написать несколько тестов для него. Но перед этим нам также нужно сделать папку с данными и несколько тестовых файлов.

Начнем с папки data. Создайте его как подпапку test. Он будет содержать тестовые файлы и будет помещен в репозиторий git. После этого нам понадобятся упомянутые три простых тестовых файла:

  • тестовые данные 1.txt, содержание: Hello, World!
  • тестовые данные 2.txt, содержание: Hello, Developer!
  • и test data.zip, созданный с помощью этой команды:
PowerShell .\path\to\test\data> Compress-Archive -Path "test data 1.txt", "test data 2.txt" -DestinationPath "./test data.zip" -Force

Настройка функций beforeEach и afterAll

Во время теста тестовые сценарии будут использовать временную папку dist для сжатия и распаковки тестовых файлов. Поскольку папка dist находится в файле .gitignore, она никогда не будет отправлена ​​в репозиторий git.

Перед каждым и после всех тестов эта папка dist будет удаляться вместе с rimraf.sync, просто чтобы убедиться, что каждый новый запуск будет проверять только что заархивированные/разархивированные файлы.

const { join } = require('path');
const rimraf = require('rimraf');
const { mkdirSync, readFileSync } = require('fs');
const { zipSync, unzipSync } = require('../src');
const TXT1 = 'test data 1.txt';
const TXT2 = 'test data 2.txt';
const ZIP = 'test data.zip';
const TXT1_DATA = join(__dirname, 'data', TXT1);
const TXT2_DATA = join(__dirname, 'data', TXT2);
const ZIP_DATA = join(__dirname, 'data', ZIP);
const DIST_FOLDER = join(__dirname, 'dist');
const UNZIPPED1 = join(__dirname, 'dist', TXT1);
const UNZIPPED2 = join(__dirname, 'dist', TXT2);
const ZIPPED = join(__dirname, 'dist', ZIP);
...
beforEach(() => {
  rimraf.sync(DIST_FOLDER);
  mkdirSync(DIST_FOLDER);
});
...
afterAll(() => {
  rirmaf.sync(DIST_FOLDER);
});

Тестирование функции unzipSync

Функция unzipSync должна разархивировать файл .zip в указанное место назначения. Он получает два параметра: path и dest. path содержит расположение ZIP-файла в виде строки, dest содержит выходное местоположение в виде строки. В следующем фрагменте кода функция unzipSync вызывается с «data/test data.zip» как path и «dist» как dest, и после распаковки теста сравнивает выходные файлы с файлами data/test data 1.txt и data/test data 2.txt:

...
test('unzip "test data.zip" with unzipSync', () => {
  unzipSync(ZIP_DATA, DIST_FOLDER);
  
  let expected = readFileSync(TXT1_DATA);
  let actual = readFileSync(UNZIPPED1);
  expect(actual).toEqual(expected);
  expected = readFileSync(TXT2_DATA);
  actual = readFileSycn(UNZIPPED2);
  expect(actual).toEqual(expected);
});
...

Тестирование функции zipSync

Функция zipSync должна заархивировать один или несколько файлов в указанный целевой файл. Он получает два параметра: path и dest. path содержит одно или несколько местоположений файла в виде строки или массива строк, dest содержит расположение выходного файла в виде строки. В следующем фрагменте кода функция zipSync вызывается с ["данными/тестовыми данными 1.txt", "данными/тестовыми данными 2.txt"] как path и «dist/test data.zip» как dest, а после сжатия нам нужно вызвать функцию unzipSync, потому что мы не можем сравнивать ZIP-файлы. Мы должны сравнить содержимое разархивированных файлов .txt с содержимым файлов data/*.txt:

...
test('zip "[test data 1.txt, test data 2.txt]" with zipSync', () => {
  zipSync([TXT1_DATA, TXT2_DATA], ZIPPED);
  unzipSync(ZIPPED, DIST_FOLDER);
  let expected = readFileSync(TXT1_DATA);
  let actual = readFileSync(UNZIPPED1);
  expect(actual).toEqual(expected);
  expected = readFileSync(TXT2_DATA);
  actual = readFileSync(UNZIPPED2);
  expect(actual).toEqual(expected);
});
...

Вот полный index.test.js:

Теперь мы можем запустить тест с помощью следующей команды:

npm test

Если все прошло хорошо, вот что мы должны увидеть в терминале:

Полный исходный код этой статьи можно найти здесь:



Если вам понравилась эта статья, поделитесь ею, похлопайте в ладоши и, самое главное, следите за обновлениями, чтобы не пропустить следующую статью, посвященную улучшению качества кода.

Спасибо за чтение!