Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module.exports = {
env: {
node: true,
es6: true,
es2024: true,
},
extends: ["eslint:recommended", "plugin:prettier/recommended"],
rules: {
Expand Down
186 changes: 92 additions & 94 deletions lib/bestzip.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ const fs = require("fs");
const path = require("path");

const archiver = require("archiver");
const async = require("async");
const glob = require("glob");
const { glob, hasMagic } = require("glob");
const which = require("which");

function hasNativeZip() {
return Boolean(which.sync("zip", { nothrow: true }));
}

function expandSources(cwd, source, done) {
async function expandSources(cwd, source) {
// options to behave more like the native zip's glob support
const globOpts = {
cwd,
Expand All @@ -25,29 +24,27 @@ function expandSources(cwd, source, done) {
noext: true, // no (a|b)
nobrace: true, // no {a,b}
};

// first handle arrays
if (Array.isArray(source)) {
return async.concat(
source,
(_source, next) => expandSources(cwd, _source, next),
done
);
const results = await Promise.all(source.map((s) => expandSources(cwd, s)));
return results.flat();
}

// then expand magic
if (typeof source !== "string") {
throw new Error(`source is (${typeof source}) `);
}
if (glob.hasMagic(source, globOpts)) {

if (hasMagic(source, globOpts)) {
// archiver uses this library but somehow ends up with different results on windows:
// archiver.glob('*') will include subdirectories, but omit their contents on windows
// so we'll use glob directly, and add all of the files it finds
glob(source, globOpts, done);
return await glob(source, globOpts);
} else {
// or just trigger the callback with the source string if there is no magic
process.nextTick(() => {
// always return an array
done(null, [source]);
});
// always return an array
return [source];
}
}

Expand All @@ -63,95 +60,96 @@ function walkDir(fullPath) {
return files.reduce((acc, cur) => acc.concat(cur), []);
}

const nativeZip = (options) =>
new Promise((resolve, reject) => {
const cwd = options.cwd || process.cwd();
const command = "zip";
expandSources(cwd, options.source, (err, sources) => {
const args = ["--quiet", "--recurse-paths", options.destination].concat(
sources
);
if (
typeof options.level == "number" &&
!isNaN(options.level) &&
options.level >= 0 &&
options.level <= 9
) {
args.splice(0, 0, "-" + options.level.toString());
}
const nativeZip = async (options) => {
const cwd = options.cwd || process.cwd();
const command = "zip";
const sources = await expandSources(cwd, options.source);

const args = ["--quiet", "--recurse-paths", options.destination].concat(
sources
);
if (
typeof options.level == "number" &&
!isNaN(options.level) &&
options.level >= 0 &&
options.level <= 9
) {
args.splice(0, 0, "-" + options.level.toString());
}

const zipProcess = cp.spawn(command, args, {
stdio: "inherit",
cwd,
});
zipProcess.on("error", reject);
zipProcess.on("close", (exitCode) => {
if (exitCode === 0) {
resolve();
} else {
// exit code 12 means "nothing to do" right?
//console.log('rejecting', zipProcess)
reject(
new Error(
`Unexpected exit code from native zip: ${exitCode}\n executed command '${command} ${args.join(
" "
)}'\n executed in directory '${cwd}'`
)
);
}
});
return new Promise((resolve, reject) => {
const zipProcess = cp.spawn(command, args, {
stdio: "inherit",
cwd,
});
zipProcess.on("error", reject);
zipProcess.on("close", (exitCode) => {
if (exitCode === 0) {
resolve();
} else {
reject(
new Error(
`Unexpected exit code from native zip: ${exitCode}\n executed command '${command} ${args.join(
" "
)}'\n executed in directory '${cwd}'`
)
);
}
});
});
};

// based on http://stackoverflow.com/questions/15641243/need-to-zip-an-entire-directory-using-node-js/18775083#18775083
const nodeZip = (options) =>
new Promise((resolve, reject) => {
const cwd = options.cwd || process.cwd();
const output = fs.createWriteStream(path.resolve(cwd, options.destination));
const archive = archiver("zip", {
zlib: { level: options.level },
});
const nodeZip = async (options) => {
const cwd = options.cwd || process.cwd();
const output = fs.createWriteStream(path.resolve(cwd, options.destination));
const archive = archiver("zip", {
zlib: { level: options.level },
});

output.on("close", () => resolvePromise());
archive.on("error", (err) => rejectPromise(err));

output.on("close", resolve);
archive.on("error", reject);

archive.pipe(output);

function addSource(source, next) {
const fullPath = path.resolve(cwd, source);
const destPath = source;
fs.stat(fullPath, function (err, stats) {
if (err) {
return next(err);
}
if (stats.isDirectory()) {
// Walk directory. Works on directories and directory symlinks.
const files = walkDir(fullPath);
files.forEach((f) => {
const subPath = f.substring(fullPath.length);
archive.file(f, {
name: destPath + subPath,
});
});
} else if (stats.isFile()) {
archive.file(fullPath, { stats: stats, name: destPath });
}
next();
archive.pipe(output);

let resolvePromise;
let rejectPromise;
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});

async function addSource(source) {
const fullPath = path.resolve(cwd, source);
const destPath = source;

const stats = await fs.promises.stat(fullPath);
if (stats.isDirectory()) {
// Walk directory. Works on directories and directory symlinks.
const files = walkDir(fullPath);
files.forEach((f) => {
const subPath = f.substring(fullPath.length);
archive.file(f, {
name: destPath + subPath,
});
});
} else if (stats.isFile()) {
archive.file(fullPath, { stats: stats, name: destPath });
}
}

expandSources(cwd, options.source, (err, expandedSources) => {
if (err) {
return reject(err);
}
async.forEach(expandedSources, addSource, (err) => {
if (err) {
return reject(err);
}
archive.finalize();
});
});
});
try {
const expandedSources = await expandSources(cwd, options.source);
for (const source of expandedSources) {
await addSource(source);
}
archive.finalize();
} catch (err) {
rejectPromise(err);
}

return promise;
};

function zip(options) {
const compatMode = typeof options === "string";
Expand Down
Loading