diff --git a/_/amazon/handlers/index.js b/_/amazon/handlers/index.js index b693f54..38de165 100644 --- a/_/amazon/handlers/index.js +++ b/_/amazon/handlers/index.js @@ -3,7 +3,7 @@ const { createHash } = require("node:crypto"); const puppeteer = require("puppeteer-core"); const chromium = require("@sparticuz/chromium"); -exports.handler = async (event, context) => { +exports.handler = async (event) => { let browser = null; try { diff --git a/package-lock.json b/package-lock.json index 6ee4a17..614be92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,25 +13,18 @@ "tar-fs": "^3.0.5" }, "devDependencies": { - "@tsconfig/node16": "^16.1.3", "@tsconfig/node20": "^20.1.4", "@tsconfig/strictest": "^2.0.5", "@types/follow-redirects": "^1.14.4", - "@types/node": "^20.12.3", + "@types/node": "^20.12.7", "@types/tar-fs": "^2.0.4", "clean-modules": "^3.0.5", - "typescript": "^5.4.3" + "typescript": "^5.4.5" }, "engines": { "node": ">= 16" } }, - "node_modules/@tsconfig/node16": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.3.tgz", - "integrity": "sha512-9nTOUBn+EMKO6rtSZJk+DcqsfgtlERGT9XPJ5PRj/HNENPCBY1yu/JEj5wT6GLtbCLBO2k46SeXDaY0pjMqypw==", - "dev": true - }, "node_modules/@tsconfig/node20": { "version": "20.1.4", "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", @@ -54,9 +47,9 @@ } }, "node_modules/@types/node": { - "version": "20.12.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.3.tgz", - "integrity": "sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -306,9 +299,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index fbc5d34..11e0851 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,10 @@ "@tsconfig/node20": "^20.1.4", "@tsconfig/strictest": "^2.0.5", "@types/follow-redirects": "^1.14.4", - "@types/node": "^20.12.3", + "@types/node": "^20.12.7", "@types/tar-fs": "^2.0.4", "clean-modules": "^3.0.5", - "typescript": "^5.4.3" + "typescript": "^5.4.5" }, "engines": { "node": ">= 16" diff --git a/source/helper.ts b/source/helper.ts index cc03c92..ff6c27e 100644 --- a/source/helper.ts +++ b/source/helper.ts @@ -1,9 +1,8 @@ -import { unlink } from "node:fs"; import { https } from "follow-redirects"; +import { unlink } from "node:fs"; import { tmpdir } from "node:os"; +import { type UrlWithStringQuery, parse } from "node:url"; import { extract } from "tar-fs"; -import { parse } from "node:url"; -import type { UrlWithStringQuery } from "node:url"; interface FollowRedirOptions extends UrlWithStringQuery { maxBodyLength: number; @@ -12,7 +11,7 @@ interface FollowRedirOptions extends UrlWithStringQuery { export const isValidUrl = (input: string) => { try { return !!new URL(input); - } catch (err) { + } catch { return false; } }; @@ -59,18 +58,18 @@ export const downloadAndExtract = async (url: string) => new Promise((resolve, reject) => { const getOptions = parse(url) as FollowRedirOptions; getOptions.maxBodyLength = 60 * 1024 * 1024; // 60mb - const destDir = `${tmpdir()}/chromium-pack`; - const extractObj = extract(destDir); + const destinationDirectory = `${tmpdir()}/chromium-pack`; + const extractObject = extract(destinationDirectory); https .get(url, (response) => { - response.pipe(extractObj); - extractObj.on("finish", () => { - resolve(destDir); + response.pipe(extractObject); + extractObject.on("finish", () => { + resolve(destinationDirectory); }); }) - .on("error", (err) => { - unlink(destDir, (_) => { - reject(err); + .on("error", (error) => { + unlink(destinationDirectory, () => { + reject(error); }); }); }); diff --git a/source/index.ts b/source/index.ts index 1e92934..979f3bc 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,3 +1,4 @@ +import { https } from "follow-redirects"; import { access, createWriteStream, @@ -5,27 +6,19 @@ import { mkdirSync, symlink, } from "node:fs"; -import { https } from "follow-redirects"; -import LambdaFS from "./lambdafs"; import { join } from "node:path"; import { URL } from "node:url"; + import { downloadAndExtract, isRunningInAwsLambda, - isValidUrl, isRunningInAwsLambdaNode20, + isValidUrl, } from "./helper"; +import { inflate } from "./lambdafs"; /** Viewport taken from https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.viewport.md */ interface Viewport { - /** - * The page width in pixels. - */ - width: number; - /** - * The page height in pixels. - */ - height: number; /** * Specify device scale factor. * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info. @@ -33,20 +26,28 @@ interface Viewport { */ deviceScaleFactor?: number; /** - * Whether the `meta viewport` tag is taken into account. + * Specify if the viewport supports touch events. * @default false */ - isMobile?: boolean; + hasTouch?: boolean; + /** + * The page height in pixels. + */ + height: number; /** * Specifies if the viewport is in landscape mode. * @default false */ isLandscape?: boolean; /** - * Specify if the viewport supports touch events. + * Whether the `meta viewport` tag is taken into account. * @default false */ - hasTouch?: boolean; + isMobile?: boolean; + /** + * The page width in pixels. + */ + width: number; } if (isRunningInAwsLambda()) { @@ -93,71 +94,7 @@ class Chromium { * If false, webgl will be disabled. * (If false, the swiftshader.tar.br file will also not extract) */ - private static graphicsMode: boolean = true; - - /** - * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it. - */ - static font(input: string): Promise { - if (process.env["HOME"] === undefined) { - process.env["HOME"] = "/tmp"; - } - - if (existsSync(`${process.env["HOME"]}/.fonts`) !== true) { - mkdirSync(`${process.env["HOME"]}/.fonts`); - } - - return new Promise((resolve, reject) => { - if (/^https?:[/][/]/i.test(input) !== true) { - input = `file://${input}`; - } - - const url = new URL(input); - const output = `${process.env["HOME"]}/.fonts/${url.pathname - .split("/") - .pop()}`; - - if (existsSync(output) === true) { - return resolve(output.split("/").pop() as string); - } - - if (url.protocol === "file:") { - access(url.pathname, (error) => { - if (error != null) { - return reject(error); - } - - symlink(url.pathname, output, (error) => { - return error != null - ? reject(error) - : resolve(url.pathname.split("/").pop() as string); - }); - }); - } else { - https.get(input, (response) => { - if (response.statusCode !== 200) { - return reject(`Unexpected status code: ${response.statusCode}.`); - } - - const stream = createWriteStream(output); - - stream.once("error", (error) => { - return reject(error); - }); - - response.on("data", (chunk) => { - stream.write(chunk); - }); - - response.once("end", () => { - stream.end(() => { - return resolve(url.pathname.split("/").pop() as string); - }); - }); - }); - } - }); - } + private static graphicsMode = true; /** * Returns a list of additional Chromium flags recommended for serverless environments. @@ -271,19 +208,19 @@ class Chromium { // Extract the required files const promises = [ - LambdaFS.inflate(`${input}/chromium.br`), - LambdaFS.inflate(`${input}/fonts.tar.br`), + inflate(`${input}/chromium.br`), + inflate(`${input}/fonts.tar.br`), ]; if (this.graphics) { // Only inflate graphics stack if needed - promises.push(LambdaFS.inflate(`${input}/swiftshader.tar.br`)); + promises.push(inflate(`${input}/swiftshader.tar.br`)); } if (isRunningInAwsLambda()) { // If running in AWS Lambda, extract more required files - promises.push(LambdaFS.inflate(`${input}/al2.tar.br`)); + promises.push(inflate(`${input}/al2.tar.br`)); } if (isRunningInAwsLambdaNode20()) { - promises.push(LambdaFS.inflate(`${input}/al2023.tar.br`)); + promises.push(inflate(`${input}/al2023.tar.br`)); } // Await all extractions @@ -292,6 +229,70 @@ class Chromium { return result.shift() as string; } + /** + * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it. + */ + static font(input: string): Promise { + if (process.env["HOME"] === undefined) { + process.env["HOME"] = "/tmp"; + } + + if (existsSync(`${process.env["HOME"]}/.fonts`) !== true) { + mkdirSync(`${process.env["HOME"]}/.fonts`); + } + + return new Promise((resolve, reject) => { + if (/^https?:\/\//i.test(input) !== true) { + input = `file://${input}`; + } + + const url = new URL(input); + const output = `${process.env["HOME"]}/.fonts/${url.pathname + .split("/") + .pop()}`; + + if (existsSync(output) === true) { + return resolve(output.split("/").pop() as string); + } + + if (url.protocol === "file:") { + access(url.pathname, (error) => { + if (error != null) { + return reject(error); + } + + symlink(url.pathname, output, (error) => { + return error == null + ? resolve(url.pathname.split("/").pop() as string) + : reject(error); + }); + }); + } else { + https.get(input, (response) => { + if (response.statusCode !== 200) { + return reject(`Unexpected status code: ${response.statusCode}.`); + } + + const stream = createWriteStream(output); + + stream.once("error", (error) => { + return reject(error); + }); + + response.on("data", (chunk) => { + stream.write(chunk); + }); + + response.once("end", () => { + stream.end(() => { + return resolve(url.pathname.split("/").pop() as string); + }); + }); + }); + } + }); + } + /** * Returns whether the graphics stack is enabled or disabled * @returns boolean @@ -309,7 +310,7 @@ class Chromium { */ public static set setGraphicsMode(value: boolean) { if (typeof value !== "boolean") { - throw new Error( + throw new TypeError( `Graphics mode must be a boolean, you entered '${value}'` ); } diff --git a/source/lambdafs.ts b/source/lambdafs.ts index b032fa2..5937733 100644 --- a/source/lambdafs.ts +++ b/source/lambdafs.ts @@ -1,75 +1,73 @@ import { createReadStream, createWriteStream, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; -import { extract } from "tar-fs"; import { createBrotliDecompress, createUnzip } from "node:zlib"; +import { extract } from "tar-fs"; -class LambdaFS { - /** - * 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. - */ - static inflate(filePath: string): Promise { - const output = filePath.includes("swiftshader") - ? tmpdir() - : join( - tmpdir(), - basename(filePath).replace( - /[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz)|br|gz)$/i, - "" - ) - ); +/** + * 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. + */ +export const inflate = (filePath: string): Promise => { + const output = filePath.includes("swiftshader") + ? tmpdir() + : join( + tmpdir(), + basename(filePath).replace( + /\.(?:t(?:ar(?:\.(?:br|gz))?|br|gz)|br|gz)$/i, + "" + ) + ); - return new Promise((resolve, reject) => { - if (filePath.includes("swiftshader")) { - if (existsSync(`${output}/libGLESv2.so`)) { - return resolve(output); - } - } else { - if (existsSync(output) === true) { - return resolve(output); - } + return new Promise((resolve, reject) => { + if (filePath.includes("swiftshader")) { + if (existsSync(`${output}/libGLESv2.so`)) { + return resolve(output); } - - let source = createReadStream(filePath, { highWaterMark: 2 ** 23 }); - let target = null; - - if (/[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz))$/i.test(filePath) === true) { - target = extract(output); - - target.once("finish", () => { - return resolve(output); - }); - } else { - target = createWriteStream(output, { mode: 0o700 }); + } else { + if (existsSync(output) === true) { + return resolve(output); } + } - source.once("error", (error: Error) => { - return reject(error); - }); + const source = createReadStream(filePath, { highWaterMark: 2 ** 23 }); + let target = null; - target.once("error", (error: Error) => { - return reject(error); - }); + if (/\.t(?:ar(?:\.(?:br|gz))?|br|gz)$/i.test(filePath) === true) { + target = extract(output); - target.once("close", () => { + target.once("finish", () => { return resolve(output); }); + } else { + target = createWriteStream(output, { mode: 0o700 }); + } - if (/(?:br|gz)$/i.test(filePath) === true) { - source - .pipe( - /br$/i.test(filePath) - ? createBrotliDecompress({ chunkSize: 2 ** 21 }) - : createUnzip({ chunkSize: 2 ** 21 }) - ) - .pipe(target); - } else { - source.pipe(target); - } + source.once("error", (error: Error) => { + return reject(error); }); - } -} -export default LambdaFS; + target.once("error", (error: Error) => { + return reject(error); + }); + + target.once("close", () => { + return resolve(output); + }); + + if (/(?:br|gz)$/i.test(filePath) === true) { + source + .pipe( + /br$/i.test(filePath) + ? createBrotliDecompress({ chunkSize: 2 ** 21 }) + : createUnzip({ chunkSize: 2 ** 21 }) + ) + .pipe(target); + } else { + source.pipe(target); + } + }); +}; + +export default inflate;