diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18531b3..346585c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,13 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 - - 10 - - 8 + - 20 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/.gitignore b/.gitignore index df65ccc..239ecff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ node_modules yarn.lock -/dest -.nyc_output -coverage diff --git a/gulpfile.js b/gulpfile.js index b97cf74..08ed162 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,33 +1,32 @@ -'use strict'; -const gulp = require('gulp'); -const gzip = require('gulp-gzip'); -const tar = require('.'); +import gulp from 'gulp'; +import gzip from 'gulp-gzip'; +import tar from './index.js'; -exports.default = () => { +export default function main() { // Tar in buffer mode - gulp.src('fixture/fixture.txt') + gulp.src('test/fixture/fixture.txt') .pipe(tar('test1.tar')) .pipe(gulp.dest('dest')); // Tar in stream mode - gulp.src('fixture/fixture.txt', {buffer: false}) + gulp.src('test/fixture/fixture.txt', {buffer: false}) .pipe(tar('test2.tar')) .pipe(gulp.dest('dest')); // Tar and gzip in buffer mode - gulp.src('fixture/fixture.txt') + gulp.src('test/fixture/fixture.txt') .pipe(tar('test3.tar')) .pipe(gzip()) .pipe(gulp.dest('dest')); // Tar and gzip in stream mode - gulp.src('fixture/fixture.txt', {buffer: false}) + gulp.src('test/fixture/fixture.txt', {buffer: false}) .pipe(tar('test4.tar')) .pipe(gzip()) .pipe(gulp.dest('dest')); // Check default parameters for tar-stream - gulp.src('fixture/fixture.txt') + gulp.src('test/fixture/fixture.txt') .pipe(tar('test_options.tar', {mtime: 0})) .pipe(gulp.dest('dest')); -}; +} diff --git a/index.js b/index.js index 36d1e71..c7f2921 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,18 @@ -'use strict'; -const path = require('path'); -const through = require('through2'); -const archiver = require('archiver'); -const PluginError = require('plugin-error'); -const Vinyl = require('vinyl'); - -module.exports = (filename, options) => { +import path from 'node:path'; +import archiver from 'archiver'; +import Vinyl from 'vinyl'; +import {gulpPlugin} from 'gulp-plugin-extras'; + +export default function gulpTar(filename, options) { if (!filename) { - throw new PluginError('gulp-tar', '`filename` required'); + throw new Error('gulp-tar: `filename` required'); } let firstFile; const archive = archiver('tar', options); - return through.obj((file, encoding, callback) => { + return gulpPlugin('gulp-tar', async file => { if (file.relative === '') { - callback(); return; } @@ -23,7 +20,7 @@ module.exports = (filename, options) => { firstFile = file; } - const nameNormalized = file.relative.replace(/\\/g, '/'); + const nameNormalized = file.relative.replaceAll('\\', '/'); if (file.isSymbolic()) { archive.symlink(nameNormalized, file.symlink); @@ -32,26 +29,24 @@ module.exports = (filename, options) => { name: nameNormalized + (file.isNull() ? '/' : ''), mode: file.stat && file.stat.mode, date: file.stat && file.stat.mtime ? file.stat.mtime : null, - ...options + ...options, }); } - - callback(); - }, function (callback) { - if (firstFile === undefined) { - callback(); - return; - } - - archive.finalize(); - - this.push(new Vinyl({ - cwd: firstFile.cwd, - base: firstFile.base, - path: path.join(firstFile.base, filename), - contents: archive - })); - - callback(); + }, { + supportsAnyType: true, + async * onFinish() { + if (firstFile === undefined) { + return; + } + + archive.finalize(); + + yield new Vinyl({ + cwd: firstFile.cwd, + base: firstFile.base, + path: path.join(firstFile.base, filename), + contents: archive, + }); + }, }); -}; +} diff --git a/license b/license index e7af2f7..fa7ceba 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Sindre Sorhus (https://sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/package.json b/package.json index 7913cc6..9a75511 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,19 @@ "description": "Create tarball from files", "license": "MIT", "repository": "sindresorhus/gulp-tar", + "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", - "url": "sindresorhus.com" + "url": "https://sindresorhus.com" }, + "type": "module", + "exports": "./index.js", "engines": { - "node": ">=8" + "node": ">=18" }, "scripts": { - "test": "xo && nyc mocha" + "test": "xo && ava" }, "files": [ "index.js" @@ -32,20 +35,19 @@ "streams" ], "dependencies": { - "archiver": "^3.1.1", - "plugin-error": "^1.0.1", - "through2": "^3.0.1", - "vinyl": "^2.1.0" + "archiver": "^6.0.1", + "gulp-plugin-extras": "^0.3.0", + "vinyl": "^3.0.0" }, "devDependencies": { + "ava": "^5.3.1", "gulp": "^4.0.2", - "gulp-gzip": "^1.2.0", - "vinyl-fs": "^3.0.3", - "mocha": "^6.2.0", - "nyc": "^14.1.1", - "rimraf": "^3.0.0", - "tar-fs": "^2.0.0", - "xo": "^0.24.0" + "gulp-gzip": "^1.4.2", + "p-event": "^6.0.0", + "rimraf": "^5.0.5", + "tar-fs": "^3.0.4", + "vinyl-fs": "^4.0.0", + "xo": "^0.56.0" }, "peerDependencies": { "gulp": ">=4" @@ -54,5 +56,8 @@ "gulp": { "optional": true } + }, + "ava": { + "serial": true } } diff --git a/readme.md b/readme.md index e6c7fc3..4cad7cd 100644 --- a/readme.md +++ b/readme.md @@ -2,22 +2,20 @@ > Create [tarball](https://en.wikipedia.org/wiki/Tar_(computing)) from files - ## Install -``` -$ npm install --save-dev gulp-tar +```sh +npm install --save-dev gulp-tar ``` - ## Usage ```js -const gulp = require('gulp'); -const tar = require('gulp-tar'); -const gzip = require('gulp-gzip'); +import gulp from 'gulp'; +import tar from 'gulp-tar'; +import gzip from 'gulp-gzip'; -exports.default = () => ( +export default () => ( gulp.src('src/*') .pipe(tar('archive.tar')) .pipe(gzip()) @@ -25,7 +23,6 @@ exports.default = () => ( ); ``` - ## API ### tar(filename, options?) @@ -34,7 +31,7 @@ exports.default = () => ( Type: `string` -Filename for the output tar archive. +The filename for the output tar archive. #### options diff --git a/test/directory.js b/test/directory.js index 337c2a5..825d9ff 100644 --- a/test/directory.js +++ b/test/directory.js @@ -1,48 +1,53 @@ -/* eslint-env mocha */ -const {createReadStream} = require('fs'); -const path = require('path'); -const zlib = require('zlib'); -const assert = require('assert'); -const gulp = require('gulp'); -const gulpZip = require('gulp-gzip'); -const tarfs = require('tar-fs'); -const rimraf = require('rimraf'); -const gulpTar = require('..'); +import {createReadStream} from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import zlib from 'node:zlib'; +import {pipeline} from 'node:stream/promises'; +import test from 'ava'; +import {pEvent} from 'p-event'; +import gulp from 'gulp'; +import gulpZip from 'gulp-gzip'; +import tarfs from 'tar-fs'; +import {rimrafSync} from 'rimraf'; +import gulpTar from '../index.js'; -it('should include directories', done => { - const onTargz = () => { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test('should include directories', async t => { + const onTargz = async () => { const filename = 'archive.tar.gz'; const fullPath = path.join(__dirname, 'dest', filename); const rs = createReadStream(fullPath); - rs.pipe(zlib.createGunzip()) - .pipe( - tarfs.extract(path.join(__dirname, 'dest-out'), { - mapStream: (filestream, header) => { - const expected = expectedNames.pop(); - assert.strictEqual(header.name, expected); - return filestream; - } - }) - ) - .on('finish', () => { - for (const directory of ['dest', 'dest-out']) { - rimraf.sync(path.join(__dirname, directory)); - } + const expectedNames = [ + 'dir-fixture/inside.txt', + 'fixture.txt', + ]; + + await pipeline( + rs, + zlib.createGunzip(), + tarfs.extract(path.join(__dirname, 'dest-out'), { + mapStream(filestream, header) { + const expected = expectedNames.pop(); + t.is(header.name, expected); + return filestream; + }, + }), + ); - done(); - }); + for (const directory of ['dest', 'dest-out']) { + rimrafSync(path.join(__dirname, directory)); + } }; - const expectedNames = [ - 'dir-fixture/inside.txt', - 'fixture.txt' - ]; + await pEvent( + gulp.src('fixture/**/*', {cwd: __dirname}) + .pipe(gulpTar('archive.tar')) + .pipe(gulpZip()) + .pipe(gulp.dest('dest', {cwd: __dirname})), + 'finish', + ); - gulp - .src('fixture/**/*', {cwd: __dirname}) - .pipe(gulpTar('archive.tar')) - .pipe(gulpZip()) - .pipe(gulp.dest('dest', {cwd: __dirname})) - .on('finish', onTargz); + await onTargz(); }); diff --git a/test/empty.js b/test/empty.js index 9d23cfd..fc49feb 100644 --- a/test/empty.js +++ b/test/empty.js @@ -1,22 +1,27 @@ -/* eslint-env mocha */ -const {throws} = require('assert'); -const gulp = require('gulp'); -const gulpZip = require('gulp-gzip'); -const gulpTar = require('..'); +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import test from 'ava'; +import {pEvent} from 'p-event'; +import gulp from 'gulp'; +import gulpZip from 'gulp-gzip'; +import gulpTar from '../index.js'; -it('should not fail on empty directory', done => { - gulp +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test('should not fail on empty directory', async t => { + const stream = gulp .src('fixture/.empty/**/*', {cwd: __dirname}) .pipe(gulpTar('archive.tar')) .pipe(gulpZip()) - .pipe(gulp.dest('dest', {cwd: __dirname})) - .on('finish', () => { - done(); - }); + .pipe(gulp.dest('dest', {cwd: __dirname})); + + await t.notThrowsAsync(pEvent(stream, 'finish')); }); -it('should fail on missing filename', () => { - throws(() => { +test('should fail on missing filename', t => { + t.throws(() => { gulpTar(); - }, /`filename` required/); + }, { + message: /`filename` required/, + }); }); diff --git a/test/main.js b/test/main.js index 503e695..83f8214 100644 --- a/test/main.js +++ b/test/main.js @@ -1,90 +1,82 @@ -'use strict'; -/* eslint-env mocha */ -const path = require('path'); -const Stream = require('stream'); -const assert = require('assert'); -const Vinyl = require('vinyl'); -const tar = require('..'); - -it('should tar files in buffer mode', callback => { +import {Buffer} from 'node:buffer'; +import {fileURLToPath} from 'node:url'; +import path from 'node:path'; +import {Readable as Stream} from 'node:stream'; +import test from 'ava'; +import {pEvent} from 'p-event'; +import Vinyl from 'vinyl'; +import tar from '../index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test('should tar files in buffer mode', async t => { const stream = tar('test.tar'); - stream.on('data', file => { - assert.strictEqual(file.path, path.join(__dirname, 'fixture', 'test.tar')); - assert.strictEqual(file.relative, 'test.tar'); - callback(); - }); - + const onData = pEvent(stream, 'data'); stream.write(new Vinyl({ cwd: __dirname, base: path.join(__dirname, 'fixture'), path: path.join(__dirname, 'fixture/fixture.txt'), - contents: Buffer.from('hello world 1') + contents: Buffer.from('hello world 1'), })); - stream.write(new Vinyl({ cwd: __dirname, base: path.join(__dirname, 'fixture'), path: path.join(__dirname, 'fixture/fixture.txt'), - contents: Buffer.from('hello world 2') + contents: Buffer.from('hello world 2'), })); - stream.end(); + + const file = await onData; + t.is(file.path, path.join(__dirname, 'fixture', 'test.tar')); + t.is(file.relative, 'test.tar'); }); -it('should tar files in stream mode', callback => { +test('should tar files in stream mode', async t => { const stream = tar('test.tar'); - const stringStream1 = new Stream.Readable(); - const stringStream2 = new Stream.Readable(); - - stringStream1.pipe = dest => { - dest.write('hello world 1'); - }; - - stringStream2.pipe = dest => { - dest.write('hello world 2'); - }; - - stream.on('data', file => { - assert.strictEqual(file.path, path.join(__dirname, 'fixture', 'test.tar')); - assert.strictEqual(file.relative, 'test.tar'); - }); - - stream.on('end', callback); + const stringStream1 = new Stream(); + const stringStream2 = new Stream(); + stringStream1._read = () => {}; + stringStream2._read = () => {}; + stringStream1.push('hello world 1'); + stringStream2.push('hello world 2'); + const onData = pEvent(stream, 'data'); + const onEnd = pEvent(stream, 'end'); stream.write(new Vinyl({ cwd: __dirname, base: path.join(__dirname, 'fixture'), path: path.join(__dirname, 'fixture/fixture.txt'), - contents: stringStream1 + contents: stringStream1, })); - stream.write(new Vinyl({ cwd: __dirname, base: path.join(__dirname, 'fixture'), path: path.join(__dirname, 'fixture/fixture.txt'), - contents: stringStream2 + contents: stringStream2, })); - stream.end(); + + const file = await onData; + t.is(file.path, path.join(__dirname, 'fixture', 'test.tar')); + t.is(file.relative, 'test.tar'); + await onEnd; }); -it('should output file.contents as a Stream', callback => { +test('should output file.contents as a Stream', async t => { const stream = tar('test.tar'); - stream.on('data', file => { - assert(file.contents instanceof Stream, 'File contents should be a Stream object'); - callback(); - }); - + const onData = pEvent(stream, 'data'); stream.write(new Vinyl({ cwd: __dirname, base: path.join(__dirname, 'fixture'), path: path.join(__dirname, 'fixture/fixture.txt'), - contents: Buffer.from('hello world') + contents: Buffer.from('hello world'), })); + stream.end(); + const file = await onData; + t.true(file.contents.readable); stream.end(); }); - diff --git a/test/symlink.js b/test/symlink.js index cd4abc4..a5b4d8a 100644 --- a/test/symlink.js +++ b/test/symlink.js @@ -1,60 +1,64 @@ -/* eslint-env mocha */ -const {createReadStream} = require('fs'); -const path = require('path'); -const zlib = require('zlib'); -const assert = require('assert'); -const gulp = require('gulp'); -const gulpZip = require('gulp-gzip'); -const tarfs = require('tar-fs'); -const rimraf = require('rimraf'); -const vinylFs = require('vinyl-fs'); -const gulpTar = require('..'); - -it('should include symlink', done => { - const onTargz = () => { +import {createReadStream} from 'node:fs'; +import path from 'node:path'; +import {createGunzip} from 'node:zlib'; +import {fileURLToPath} from 'node:url'; +import test from 'ava'; +import {pEvent} from 'p-event'; +import tarfs from 'tar-fs'; +import {rimraf} from 'rimraf'; +import vinylFs from 'vinyl-fs'; +import gulp from 'gulp'; +import gulpZip from 'gulp-gzip'; +import gulpTar from '../index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test('should include symlink', async t => { + const onTargz = async () => { const filename = 'archive.tar.gz'; const fullPath = path.join(__dirname, 'dest', filename); const rs = createReadStream(fullPath); - rs.pipe(zlib.createGunzip()) - .pipe( - tarfs.extract(path.join(__dirname, 'dest-out'), { - map: header => { - const expected = expectedNames.pop(); - assert.strictEqual(header.name, expected.name); - assert.strictEqual(header.type, expected.type); - if (header.type === 'symlink') { - assert.strictEqual(header.linkname, expected.linkname); - } - - return header; + const expectedHeaders = { + 'fixture/.symlink/symlink.txt': {type: 'symlink', linkname: '../fixture.txt'}, + 'fixture/dir-fixture/inside.txt': {type: 'file'}, + 'fixture/fixture.txt': {type: 'file'}, + 'fixture/dir-fixture/': {type: 'directory'}, + }; + + const extractStream = tarfs.extract(path.join(__dirname, 'dest-out'), { + map(header) { + const expected = expectedHeaders[header.name]; + if (expected) { + t.is(header.type, expected.type); + if (header.type === 'symlink') { + t.is(header.linkname, expected.linkname); } - }) - ) - .on('finish', () => { - for (const directory of ['dest', 'dest-out']) { - rimraf.sync(path.join(__dirname, directory)); } - done(); - }); - }; + return header; + }, + }); - const expectedNames = [ - {name: 'fixture/.symlink/symlink.txt', type: 'symlink', linkname: '../fixture.txt'}, - {name: 'fixture/dir-fixture/inside.txt', type: 'file'}, - {name: 'fixture/fixture.txt', type: 'file'}, - {name: 'fixture/dir-fixture/', type: 'directory'} - ]; + const finalStream = rs.pipe(createGunzip()).pipe(extractStream); - vinylFs + await pEvent(finalStream, 'finish'); + + for (const directory of ['dest', 'dest-out']) { + await rimraf(path.join(__dirname, directory)); // eslint-disable-line no-await-in-loop + } + }; + + const archiveStream = vinylFs .src(['fixture/**/*', 'fixture/.symlink/**/*'], { cwd: __dirname, base: __dirname, - resolveSymlinks: false + resolveSymlinks: false, }) .pipe(gulpTar('archive.tar')) .pipe(gulpZip()) - .pipe(gulp.dest('dest', {cwd: __dirname})) - .on('finish', onTargz); + .pipe(gulp.dest('dest', {cwd: __dirname})); + + await pEvent(archiveStream, 'finish'); + await onTargz(); });