next-image-export-optimizer
Version:
Optimizes all static images for Next.js static HTML export functionality
172 lines (171 loc) • 8.77 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadImagesInBatches = downloadImagesInBatches;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const http = require("http");
const https = require("https");
const urlModule = require("url"); // Import url module to parse the url
async function downloadImage(url, filename, folder) {
return new Promise((resolve, reject) => {
// Choose the right http library:
const httpLib = urlModule.parse(url).protocol === "http:" ? http : https;
const request = httpLib.get(url, function (response) {
if (response.statusCode !== 200) {
console.error(`Error: Unable to download ${url} (status code: ${response.statusCode}).`);
reject(new Error(`Status code: ${response.statusCode}`));
return;
}
// check if the file is a valid image by checking the content type
if (!response.headers["content-type"].startsWith("image/") &&
!response.headers["content-type"].startsWith("application/octet-stream")) {
console.error(`Error: Unable to download ${url} (invalid content type: ${response.headers["content-type"]}).`);
reject(new Error(`Invalid content type: ${response.headers["content-type"]}`));
return;
}
// Extract image format from response headers
const contentType = response.headers["content-type"];
let imageFormat = contentType.split("/").pop();
// Further split on semicolon (;) if exists
if (imageFormat.includes(";")) {
imageFormat = imageFormat.split(";")[0];
}
// Further split on plus (+) if exists, e.g. image/svg+xml
if (imageFormat.includes("+")) {
imageFormat = imageFormat.split("+")[0];
}
// Check for jpeg and change it to jpg if necessary
if (imageFormat === "jpeg") {
imageFormat = "jpg";
}
// Check if filename already has an extension that matches the image format
const regex = new RegExp(`.${imageFormat}$`, "i");
const hasMatchingExtension = regex.test(filename);
// Add appropriate extension to filename based on image format
const formattedFilename = hasMatchingExtension
? filename
: `${filename}.${imageFormat}`;
fs_1.default.access(folder, fs_1.default.constants.W_OK, function (err) {
if (err) {
console.error(`Error: Unable to write to ${folder} (${err.message}).`);
reject(err);
return;
}
// on close, check the file size and reject if it's 0 otherwise resolve
response
.pipe(fs_1.default.createWriteStream(formattedFilename))
.on("error", function (err) {
console.error(`Error: Unable to save ${formattedFilename} (${err.message}).`);
reject(err);
})
.on("close", function () {
fs_1.default.stat(formattedFilename, function (err, stats) {
if (err) {
console.error(`Error: Unable to get the size of ${formattedFilename} (${err.message}).`);
reject(err);
return;
}
if (stats.size === 0) {
console.error(`Error: Unable to save ${formattedFilename} (empty file).`);
reject(new Error("Empty file"));
return;
}
// to cache the image locally, we store a file with the same name as the image, but with a .lastUpdated extension and the timestamp
storeLastUpdated({
basePath: folder,
file: formattedFilename,
dirPathWithoutBasePath: "",
fullPath: formattedFilename,
});
resolve();
});
});
});
});
request.on("error", (err) => {
console.error(`Error: Unable to download ${url}.`, err);
reject(err);
});
});
}
async function downloadImagesInBatches(imagesURLs, imageFileNames, folder, batchSize, remoteImageCacheTTL) {
let downloadedImages = 0;
let cachedImages = 0;
const batches = Math.ceil(imagesURLs.length / batchSize); // determine the number of batches
for (let i = 0; i < batches; i++) {
const start = i * batchSize; // calculate the start index of the batch
const end = Math.min(imagesURLs.length, start + batchSize); // calculate the end index of the batch
const batchURLs = imagesURLs.slice(start, end); // slice the URLs for the current batch
const batchFileNames = imageFileNames.slice(start, end); // slice the file names for the current batch
for (let j = 0; j < batchFileNames.length; j++) {
if (batchFileNames[j].fullPath === undefined) {
console.error(`Error: Unable to download ${batchURLs[i]} (fullPath is undefined).`);
return;
}
}
const promises = batchURLs.map((url, index) => {
const file = batchFileNames[index];
if (file.fullPath === undefined) {
console.error(`Error: Unable to download ${url} (fullPath is undefined).`);
return Promise.resolve();
}
// check if the image was downloaded before and if it's still valid
// if it's valid, skip downloading it again
// if there is no .lastUpdated file, download the image
// if there is a .lastUpdated file, check if it's older than the image
// if it's older, download the image
const lastUpdatedFilename = `${file.file}.lastUpdated`;
const lastUpdatedPath = path_1.default.join(file.basePath, lastUpdatedFilename);
let skipDownload = false;
if (fs_1.default.existsSync(lastUpdatedPath) && fs_1.default.existsSync(file.fullPath)) {
const lastUpdated = fs_1.default.readFileSync(lastUpdatedPath, "utf8");
const lastUpdatedTimestamp = parseInt(lastUpdated);
const now = Date.now();
const timeDifferenceInSeconds = (now - lastUpdatedTimestamp) / 1000;
if (timeDifferenceInSeconds < remoteImageCacheTTL) {
// console.log(
// `Skipping download of ${file.file} because it was downloaded ${timeDifferenceInSeconds} seconds ago.`
// );
skipDownload = true;
cachedImages++;
}
}
else {
// console.log("No .lastUpdated file found");
}
if (skipDownload) {
return Promise.resolve();
}
downloadedImages++;
return downloadImage(url, file.fullPath, folder);
}); // create an array of promises for downloading images in the batch
try {
await Promise.all(promises); // download images in parallel for the current batch
downloadedImages > 0 &&
console.log(`Downloaded ${downloadedImages} remote image${downloadedImages > 1 ? "s" : ""}.`);
cachedImages > 0 &&
console.log(`Used ${cachedImages} cached remote image${cachedImages > 1 ? "s" : ""}.`);
}
catch (err) {
console.error(`Error: Unable to download remote images (${err.message}).`);
throw err;
}
}
}
const storeLastUpdated = (file) => {
// to cache the image locally, we need to know last time it was downloaded
// so we can check if it's still valid
// store a file with the same name as the image, but with a .lastUpdated extension and the timestamp
const lastUpdated = Date.now();
const lastUpdatedFilename = `${file.file}.lastUpdated`;
try {
fs_1.default.writeFileSync(lastUpdatedFilename, lastUpdated.toString());
}
catch (error) {
console.error(`Error writing the cache file for ${lastUpdatedFilename}: `, error);
throw error;
}
};
;