@sparticuz/chromium
Version:
Chromium Binary for Serverless Platforms
171 lines (170 loc) • 6.13 kB
JavaScript
import fr from "follow-redirects";
import { access, createWriteStream, rm, symlink } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { extract } from "tar-fs";
/**
* Creates a symlink to a file
*/
export const createSymlink = (source, target) => {
return new Promise((resolve, reject) => {
access(source, (error) => {
if (error) {
reject(error);
return;
}
symlink(source, target, (error) => {
/* c8 ignore next */
if (error) {
/* c8 ignore next 3 */
reject(error);
return;
}
resolve();
});
});
});
};
/**
* Downloads a file from a URL
*/
export const downloadFile = (url, outputPath) => {
return new Promise((resolve, reject) => {
const stream = createWriteStream(outputPath);
stream.once("error", reject);
fr.https
.get(url, (response) => {
if (response.statusCode !== 200) {
stream.close();
reject(new Error(
/* c8 ignore next 2 */
`Unexpected status code: ${response.statusCode?.toFixed(0) ?? "UNK"}.`));
return;
}
// Pipe directly to file rather than manually writing chunks
// This is more efficient and uses less memory
response.pipe(stream);
// Listen for completion
stream.once("finish", () => {
stream.close();
resolve();
});
// Handle response errors
response.once("error", (error) => {
/* c8 ignore next 2 */
stream.close();
reject(error);
});
})
/* c8 ignore next 3 */
.on("error", (error) => {
stream.close();
reject(error);
});
});
};
/**
* Adds the proper folders to the environment
* @param baseLibPath the path to this packages lib folder
*/
export const setupLambdaEnvironment = (baseLibPath) => {
// If the FONTCONFIG_PATH is not set, set it to /tmp/fonts
process.env["FONTCONFIG_PATH"] ??= join(tmpdir(), "fonts");
// Set up Home folder if not already set
process.env["HOME"] ??= tmpdir();
// If LD_LIBRARY_PATH is undefined, set it to baseLibPath, otherwise, add it
if (process.env["LD_LIBRARY_PATH"] === undefined) {
process.env["LD_LIBRARY_PATH"] = baseLibPath;
}
else if (!process.env["LD_LIBRARY_PATH"].startsWith(baseLibPath)) {
process.env["LD_LIBRARY_PATH"] = [
baseLibPath,
...new Set(process.env["LD_LIBRARY_PATH"].split(":")),
].join(":");
}
};
/**
* Determines if the input is a valid URL
* @param input the input to check
* @returns boolean indicating if the input is a valid URL
*/
export const isValidUrl = (input) => {
try {
return Boolean(new URL(input));
}
catch {
return false;
}
};
/**
* Determines if the running instance is inside an Amazon Linux 2023 container,
* AWS_EXECUTION_ENV is for native Lambda instances
* AWS_LAMBDA_JS_RUNTIME is for netlify instances
* CODEBUILD_BUILD_IMAGE is for CodeBuild instances
* VERCEL is for Vercel Functions (Node 20 or later enables an AL2023-compatible environment).
* @returns boolean indicating if the running instance is inside a Lambda container with nodejs20
*/
export const isRunningInAmazonLinux2023 = (nodeMajorVersion) => {
const awsExecEnv = process.env["AWS_EXECUTION_ENV"] ?? "";
const awsLambdaJsRuntime = process.env["AWS_LAMBDA_JS_RUNTIME"] ?? "";
const codebuildImage = process.env["CODEBUILD_BUILD_IMAGE"] ?? "";
// Check for explicit version substrings, returns on first match
if (awsExecEnv.includes("20.x") ||
awsExecEnv.includes("22.x") ||
awsExecEnv.includes("24.x") ||
awsLambdaJsRuntime.includes("20.x") ||
awsLambdaJsRuntime.includes("22.x") ||
awsLambdaJsRuntime.includes("24.x") ||
codebuildImage.includes("nodejs20") ||
codebuildImage.includes("nodejs22") ||
codebuildImage.includes("nodejs24")) {
return true;
}
// Vercel: Node 20+ is AL2023 compatible
if (process.env["VERCEL"] && nodeMajorVersion >= 20) {
return true;
}
return false;
};
export const downloadAndExtract = async (url) => {
const getOptions = new URL(url);
// Increase the max body length to 60MB for larger files
getOptions.maxBodyLength = 60 * 1024 * 1024;
const destDir = join(tmpdir(), "chromium-pack");
return new Promise((resolve, reject) => {
const extractObj = extract(destDir);
// Setup error handlers for better cleanup
/* c8 ignore next 5 */
const cleanupOnError = (err) => {
rm(destDir, { force: true, recursive: true }, () => {
reject(err);
});
};
// Attach error handler to extract stream
extractObj.once("error", cleanupOnError);
// Handle extraction completion
extractObj.once("finish", () => {
resolve(destDir);
});
const req = fr.https.get(url, (response) => {
/* c8 ignore next */
if (response.statusCode !== 200) {
/* c8 ignore next 9 */
reject(new Error(`Unexpected status code: ${response.statusCode?.toFixed(0) ?? "UNK"}.`));
return;
}
// Pipe the response directly to the extraction stream
response.pipe(extractObj);
// Handle response errors
response.once("error", cleanupOnError);
});
// Handle request errors
req.once("error", cleanupOnError);
// Set a timeout to avoid hanging requests
req.setTimeout(60 * 1000, () => {
/* c8 ignore next 2 */
req.destroy();
cleanupOnError(new Error("Request timeout"));
});
});
};