@sparticuz/chromium
Version:
Chromium Binary for Serverless Platforms
242 lines (241 loc) • 9.49 kB
JavaScript
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
let node_fs = require("node:fs");
let node_os = require("node:os");
let node_path = require("node:path");
let node_stream = require("node:stream");
let node_stream_promises = require("node:stream/promises");
let tar_fs = require("tar-fs");
let node_zlib = require("node:zlib");
//#region source/helper.ts
/**
* Adds the proper folders to the environment
* @param baseLibPath the path to this packages lib folder
*/
const setupLambdaEnvironment = (baseLibPath) => {
process.env["FONTCONFIG_PATH"] ??= (0, node_path.join)((0, node_os.tmpdir)(), "fonts");
process.env["HOME"] ??= (0, node_os.tmpdir)();
if (process.env["LD_LIBRARY_PATH"] === void 0) 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
*/
const isValidUrl = (input) => {
try {
const url = new URL(input);
if (url.protocol === "https:") return true;
if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]")) return true;
return false;
} 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
*/
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"] ?? "";
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;
if (process.env["VERCEL"] && nodeMajorVersion >= 20) return true;
return false;
};
const downloadAndExtract = async (url) => {
const destDir = (0, node_path.join)((0, node_os.tmpdir)(), "chromium-pack");
const response = await fetch(url, {
redirect: "follow",
signal: AbortSignal.timeout(3e5)
});
if (!response.ok) throw new Error(`Unexpected status code: ${String(response.status)}.`);
if (!response.body) throw new Error("Response body is empty.");
try {
await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(response.body), (0, tar_fs.extract)(destDir));
} catch (error) {
await new Promise((resolve) => {
(0, node_fs.rm)(destDir, {
force: true,
recursive: true
}, () => {
resolve();
});
});
throw error;
}
return destDir;
};
//#endregion
//#region source/lambdafs.ts
/**
* Decompresses a (tarballed) Brotli or Gzip compressed file and returns the path to the decompressed file/folder.
*
* @param filePath Path of the file to decompress.
*/
const inflate = (filePath) => {
const output = filePath.includes("swiftshader") ? (0, node_os.tmpdir)() : (0, node_path.join)((0, node_os.tmpdir)(), (0, node_path.basename)(filePath).replace(/\.(?:t(?:ar(?:\.(?:br|gz))?|br|gz)|br|gz)$/i, ""));
return new Promise((resolve, reject) => {
if (filePath.includes("swiftshader")) {
if ((0, node_fs.existsSync)(`${output}/libGLESv2.so`)) {
resolve(output);
return;
}
} else if ((0, node_fs.existsSync)(output)) {
resolve(output);
return;
}
const isBrotli = /br$/i.test(filePath);
const isGzip = /gz$/i.test(filePath);
const isTar = /\.t(?:ar(?:\.(?:br|gz))?|br|gz)$/i.test(filePath);
const source = (0, node_fs.createReadStream)(filePath, { highWaterMark: 2 ** 22 });
let target;
const handleError = (error) => {
reject(error);
};
source.once("error", handleError);
if (isTar) {
target = (0, tar_fs.extract)(output);
target.once("finish", () => {
resolve(output);
});
} else {
target = (0, node_fs.createWriteStream)(output, { mode: 448 });
target.once("close", () => {
resolve(output);
});
}
target.once("error", handleError);
if (isBrotli || isGzip) {
const decompressor = isBrotli ? (0, node_zlib.createBrotliDecompress)({ chunkSize: 2 ** 21 }) : (0, node_zlib.createUnzip)({ chunkSize: 2 ** 21 });
decompressor.once("error", handleError);
source.pipe(decompressor).pipe(target);
} else source.pipe(target);
});
};
//#endregion
//#region source/paths.cjs.ts
/**
* Get the bin directory path for CommonJS modules
*/
function getBinPath() {
return (0, node_path.join)((0, node_path.dirname)(__filename), "..", "..", "bin");
}
//#endregion
//#region source/index.ts
const nodeMajorVersion = Number.parseInt(process.versions.node.split(".")[0] ?? "");
if (isRunningInAmazonLinux2023(nodeMajorVersion)) setupLambdaEnvironment((0, node_path.join)((0, node_os.tmpdir)(), "al2023", "lib"));
var Chromium = class {
/**
* Returns a list of additional Chromium flags recommended for serverless environments.
* The canonical list of flags can be found on https://peter.sh/experiments/chromium-command-line-switches/.
* Most of below can be found here: https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
*/
static get args() {
const chromiumFlags = [
"--ash-no-nudges",
"--disable-domain-reliability",
"--disable-print-preview",
"--disk-cache-size=33554432",
"--no-default-browser-check",
"--no-pings",
"--single-process",
"--font-render-hinting=none"
];
const chromiumDisableFeatures = [
"AudioServiceOutOfProcess",
"IsolateOrigins",
"site-per-process"
];
const chromiumEnableFeatures = ["SharedArrayBuffer"];
const graphicsFlags = ["--ignore-gpu-blocklist", "--in-process-gpu"];
if (this.graphics) graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader");
else graphicsFlags.push("--disable-webgl");
const insecureFlags = [
"--allow-running-insecure-content",
"--disable-setuid-sandbox",
"--disable-site-isolation-trials",
"--disable-web-security"
];
const headlessFlags = [
"--headless='shell'",
"--no-sandbox",
"--no-zygote"
];
return [
...chromiumFlags,
`--disable-features=${[...chromiumDisableFeatures].join(",")}`,
`--enable-features=${[...chromiumEnableFeatures].join(",")}`,
...graphicsFlags,
...insecureFlags,
...headlessFlags
];
}
/**
* Returns whether the graphics stack is enabled or disabled
* @returns boolean
*/
static get graphics() {
return this.graphicsMode;
}
/**
* Sets whether the graphics stack is enabled or disabled.
* @param true means the stack is enabled. WebGL will work.
* @param false means that the stack is disabled. WebGL will not work.
* @default true
*/
static set setGraphicsMode(value) {
if (typeof value !== "boolean") throw new TypeError(`Graphics mode must be a boolean, you entered '${String(value)}'`);
this.graphicsMode = value;
}
/**
* If true, the graphics stack and webgl is enabled,
* If false, webgl will be disabled.
* (If false, the swiftshader.tar.br file will also not extract)
*/
static graphicsMode = true;
/**
* Inflates the included version of Chromium
* @param input The location of the `bin` folder
* @returns The path to the `chromium` binary
*/
static async executablePath(input) {
/**
* If the `chromium` binary already exists in /tmp/chromium, return it.
*/
if ((0, node_fs.existsSync)((0, node_path.join)((0, node_os.tmpdir)(), "chromium"))) return (0, node_path.join)((0, node_os.tmpdir)(), "chromium");
/**
* If input is a valid URL, download and extract the file. It will extract to /tmp/chromium-pack
* and executablePath will be recursively called on that location, which will then extract
* the brotli files to the correct locations
*/
if (input && isValidUrl(input)) return this.executablePath(await downloadAndExtract(input));
/**
* If input is defined, use that as the location of the brotli files,
* otherwise, the default location is ../../bin.
* A custom location is needed for workflows that using custom packaging.
*/
input ??= getBinPath();
if (!(0, node_fs.existsSync)(input)) throw new Error(`The input directory "${input}" does not exist. If you are using a bundler (esbuild, webpack, etc.), you must externalize @sparticuz/chromium so it is not relocated. See: https://github.com/Sparticuz/chromium#bundler-configuration`);
const promises = [
inflate((0, node_path.join)(input, "chromium.br")),
inflate((0, node_path.join)(input, "fonts.tar.br")),
inflate((0, node_path.join)(input, "swiftshader.tar.br"))
];
if (isRunningInAmazonLinux2023(nodeMajorVersion)) promises.push(inflate((0, node_path.join)(input, "al2023.tar.br")));
return (await Promise.all(promises)).shift();
}
};
//#endregion
exports.default = Chromium;
exports.inflate = inflate;
exports.setupLambdaEnvironment = setupLambdaEnvironment;
module.exports = module.exports.default;