diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b95879e5..f645b0ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,9 @@ jobs: ng-schematics: - '.github/workflows/ci.yml' - 'packages/ng-schematics/**' + browsers: + - '.github/workflows/ci.yml' + - 'packages/browsers/**' deploy-docs: needs: check-changes @@ -368,3 +371,38 @@ jobs: run: npm ci --ignore-scripts - name: Run tests run: npm run test --workspace @puppeteer/ng-schematics + + browsers-tests: + name: Browsers tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + needs: check-changes + if: ${{ contains(fromJSON(needs.check-changes.outputs.changes), 'browsers') }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node.js + uses: actions/setup-node@v3.5.1 + with: + cache: npm + node-version: latest + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Run tests + run: npm run test --workspace @puppeteer/browsers + + browsers-tests-required: + name: '[Required] Test the browsers packages' + needs: [check-changes, browsers-tests] + runs-on: ubuntu-latest + if: ${{ always() }} + steps: + - if: ${{ needs.browsers-tests.result != 'success' && contains(fromJSON(needs.check-changes.outputs.changes), 'browsers') }} + run: 'exit 1' + - run: 'exit 0' diff --git a/package-lock.json b/package-lock.json index 4174c622..8a26a129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1385,6 +1385,10 @@ "resolved": "test", "link": true }, + "node_modules/@puppeteer/browsers": { + "resolved": "packages/browsers", + "link": true + }, "node_modules/@puppeteer/ng-schematics": { "resolved": "packages/ng-schematics", "link": true @@ -8678,6 +8682,39 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/browsers": { + "name": "@puppeteer/browsers", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3" + }, + "devDependencies": { + "@types/node": "^14.15.0" + }, + "engines": { + "node": ">=14.1.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/browsers/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "dev": true + }, "packages/ng-schematics": { "name": "@puppeteer/ng-schematics", "version": "0.1.0", @@ -10040,6 +10077,26 @@ } } }, + "@puppeteer/browsers": { + "version": "file:packages/browsers", + "requires": { + "@types/node": "^14.15.0", + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3" + }, + "dependencies": { + "@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "dev": true + } + } + }, "@puppeteer/ng-schematics": { "version": "file:packages/ng-schematics", "requires": { diff --git a/packages/browsers/.mocharc.cjs b/packages/browsers/.mocharc.cjs new file mode 100644 index 00000000..deddacfc --- /dev/null +++ b/packages/browsers/.mocharc.cjs @@ -0,0 +1,6 @@ +module.exports = { + logLevel: 'debug', + spec: 'test/build/**/*.spec.js', + exit: !!process.env.CI, + reporter: 'spec', +}; diff --git a/packages/browsers/README.md b/packages/browsers/README.md new file mode 100644 index 00000000..1a04058b --- /dev/null +++ b/packages/browsers/README.md @@ -0,0 +1,3 @@ +# @puppeteer/browsers + +TODO diff --git a/packages/browsers/package.json b/packages/browsers/package.json new file mode 100644 index 00000000..ad638765 --- /dev/null +++ b/packages/browsers/package.json @@ -0,0 +1,81 @@ +{ + "name": "@puppeteer/browsers", + "version": "0.0.1", + "description": "Download and launch browsers", + "scripts": { + "build": "wireit", + "build:test": "wireit", + "clean": "tsc --build --clean && rimraf lib", + "test": "wireit" + }, + "wireit": { + "build": { + "command": "tsc -b", + "files": [ + "src/**/*.ts", + "tsconfig.json" + ], + "output": [ + "lib/**" + ] + }, + "build:test": { + "command": "tsc -b test/src/tsconfig.json", + "files": [ + "test/**/*.ts", + "test/src/tsconfig.json" + ], + "output": [ + "test/build/**" + ], + "dependencies": [ + "build" + ] + }, + "test": { + "command": "mocha", + "files": [ + ".mocharc.cjs" + ], + "dependencies": [ + "build:test" + ] + } + }, + "keywords": [ + "puppeteer", + "browsers" + ], + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/browsers" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14.1.0" + }, + "files": [ + "lib", + "!*.tsbuildinfo" + ], + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3" + }, + "devDependencies": { + "@types/node": "^14.15.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } +} diff --git a/packages/browsers/src/browsers/browsers.ts b/packages/browsers/src/browsers/browsers.ts new file mode 100644 index 00000000..0faa826d --- /dev/null +++ b/packages/browsers/src/browsers/browsers.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chrome from './chrome.js'; +import * as firefox from './firefox.js'; +import {Browser, BrowserPlatform} from './types.js'; + +export const downloadUrls = { + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; + +export {Browser, BrowserPlatform}; diff --git a/packages/browsers/src/browsers/chrome.ts b/packages/browsers/src/browsers/chrome.ts new file mode 100644 index 00000000..c750620a --- /dev/null +++ b/packages/browsers/src/browsers/chrome.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {BrowserPlatform} from './types.js'; + +function archive(platform: BrowserPlatform, revision: string): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'chrome-linux'; + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + return 'chrome-mac'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + // Windows archive name changed at r591479. + return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; + } +} + +function folder(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'Linux_x64'; + case BrowserPlatform.MAC_ARM: + return 'Mac_Arm'; + case BrowserPlatform.MAC: + return 'Mac'; + case BrowserPlatform.WIN32: + return 'Win'; + case BrowserPlatform.WIN64: + return 'Win_x64'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + revision: string, + baseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots' +): string { + return `${baseUrl}/${folder(platform)}/${revision}/${archive( + platform, + revision + )}.zip`; +} diff --git a/packages/browsers/src/browsers/firefox.ts b/packages/browsers/src/browsers/firefox.ts new file mode 100644 index 00000000..b63a5013 --- /dev/null +++ b/packages/browsers/src/browsers/firefox.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {BrowserPlatform} from './types.js'; + +function archive(platform: BrowserPlatform, revision: string): string { + switch (platform) { + case BrowserPlatform.LINUX: + return `firefox-${revision}.en-US.${platform}-x86_64.tar.bz2`; + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + return `firefox-${revision}.en-US.mac.dmg`; + case BrowserPlatform.WIN32: + return `firefox-${revision}.en-US.${platform}.zip`; + case BrowserPlatform.WIN64: + return `firefox-${revision}.en-US.${platform}.zip`; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + revision: string, + baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central' +): string { + return `${baseUrl}/${archive(platform, revision)}`; +} diff --git a/packages/browsers/src/browsers/types.ts b/packages/browsers/src/browsers/types.ts new file mode 100644 index 00000000..4cc46d19 --- /dev/null +++ b/packages/browsers/src/browsers/types.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chrome from './chrome.js'; +import * as firefox from './firefox.js'; + +/** + * Supported browsers. + */ +export enum Browser { + CHROME = 'chrome', + FIREFOX = 'firefox', +} + +/** + * Platform names used to identify a OS platfrom x architecture combination in the way + * that is relevant for the browser download. + */ +export enum BrowserPlatform { + LINUX = 'linux', + MAC = 'mac', + MAC_ARM = 'mac_arm', + WIN32 = 'win32', + WIN64 = 'win64', +} + +export const downloadUrls = { + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; diff --git a/packages/browsers/src/debug.ts b/packages/browsers/src/debug.ts new file mode 100644 index 00000000..eee0a347 --- /dev/null +++ b/packages/browsers/src/debug.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import debug from 'debug'; + +export {debug}; diff --git a/packages/browsers/src/fetch.ts b/packages/browsers/src/fetch.ts new file mode 100644 index 00000000..870b8711 --- /dev/null +++ b/packages/browsers/src/fetch.ts @@ -0,0 +1,171 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {existsSync} from 'fs'; +import {mkdir, unlink} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import {debug} from './debug.js'; +import {Browser, BrowserPlatform, downloadUrls} from './browsers/browsers.js'; +import {downloadFile, headHttpRequest} from './httpUtil.js'; +import assert from 'assert'; +import {unpackArchive} from './fileUtil.js'; + +const debugFetch = debug('puppeteer:browsers:fetcher'); + +/** + * @public + */ +export interface Options { + /** + * Determines the path to download browsers to. + */ + outputDir: string; + /** + * Determines which platform the browser will be suited for. + * + * @defaultValue Auto-detected. + */ + platform?: BrowserPlatform; + /** + * Determines which browser to fetch. + */ + browser: Browser; + /** + * Determines which revision to dowloand. Revision should uniquely identify + * binaries and they are used for caching. + */ + revision: string; + /** + * Provides information about the progress of the download. + */ + progressCallback?: (downloadedBytes: number, totalBytes: number) => void; +} + +export type InstalledBrowser = { + path: string; + browser: Browser; + revision: string; + platform: BrowserPlatform; +}; + +export async function fetch(options: Options): Promise { + options.platform ??= detectPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const url = getDownloadUrl( + options.browser, + options.platform, + options.revision + ); + const fileName = url.toString().split('/').pop(); + assert(fileName, `A malformed download URL was found: ${url}.`); + const archivePath = path.join(options.outputDir, fileName); + const outputPath = path.resolve( + options.outputDir, + `${options.platform}-${options.revision}` + ); + if (existsSync(outputPath)) { + return { + path: outputPath, + browser: options.browser, + platform: options.platform, + revision: options.revision, + }; + } + if (!existsSync(options.outputDir)) { + await mkdir(options.outputDir, {recursive: true}); + } + try { + debugFetch(`Downloading binary from ${url}`); + await downloadFile(url, archivePath, options.progressCallback); + debugFetch(`Installing ${archivePath} to ${outputPath}`); + await unpackArchive(archivePath, outputPath); + } finally { + if (existsSync(archivePath)) { + await unlink(archivePath); + } + } + return { + path: outputPath, + browser: options.browser, + platform: options.platform, + revision: options.revision, + }; +} + +export async function canFetch(options: Options): Promise { + options.platform ??= detectPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + return await headHttpRequest( + getDownloadUrl(options.browser, options.platform, options.revision) + ); +} + +/** + * Windows 11 is identified by the version 10.0.22000 or greater + * @internal + */ +function isWindows11(version: string): boolean { + const parts = version.split('.'); + if (parts.length > 2) { + const major = parseInt(parts[0] as string, 10); + const minor = parseInt(parts[1] as string, 10); + const patch = parseInt(parts[2] as string, 10); + return ( + major > 10 || + (major === 10 && minor > 0) || + (major === 10 && minor === 0 && patch >= 22000) + ); + } + return false; +} + +function detectPlatform(): BrowserPlatform | undefined { + const platform = os.platform(); + switch (platform) { + case 'darwin': + return os.arch() === 'arm64' + ? BrowserPlatform.MAC_ARM + : BrowserPlatform.MAC; + case 'linux': + return BrowserPlatform.LINUX; + case 'win32': + return os.arch() === 'x64' || + // Windows 11 for ARM supports x64 emulation + (os.arch() === 'arm64' && isWindows11(os.release())) + ? BrowserPlatform.WIN64 + : BrowserPlatform.WIN32; + default: + return undefined; + } +} + +function getDownloadUrl( + browser: Browser, + platform: BrowserPlatform, + revision: string +): URL { + return new URL(downloadUrls[browser](platform, revision)); +} diff --git a/packages/browsers/src/fileUtil.ts b/packages/browsers/src/fileUtil.ts new file mode 100644 index 00000000..29614ec7 --- /dev/null +++ b/packages/browsers/src/fileUtil.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import {exec as execChildProcess} from 'child_process'; +import extractZip from 'extract-zip'; +import {createReadStream} from 'fs'; +import {mkdir, readdir} from 'fs/promises'; +import tar from 'tar-fs'; +import bzip from 'unbzip2-stream'; +import {promisify} from 'util'; + +const exec = promisify(execChildProcess); + +/** + * @internal + */ +export async function unpackArchive( + archivePath: string, + folderPath: string +): Promise { + if (archivePath.endsWith('.zip')) { + await extractZip(archivePath, {dir: folderPath}); + } else if (archivePath.endsWith('.tar.bz2')) { + await extractTar(archivePath, folderPath); + } else if (archivePath.endsWith('.dmg')) { + await mkdir(folderPath); + await installDMG(archivePath, folderPath); + } else { + throw new Error(`Unsupported archive format: ${archivePath}`); + } +} + +/** + * @internal + */ +function extractTar(tarPath: string, folderPath: string): Promise { + return new Promise((fulfill, reject) => { + const tarStream = tar.extract(folderPath); + tarStream.on('error', reject); + tarStream.on('finish', fulfill); + const readStream = createReadStream(tarPath); + readStream.pipe(bzip()).pipe(tarStream); + }); +} + +/** + * @internal + */ +async function installDMG(dmgPath: string, folderPath: string): Promise { + const {stdout} = await exec( + `hdiutil attach -nobrowse -noautoopen "${dmgPath}"` + ); + + const volumes = stdout.match(/\/Volumes\/(.*)/m); + if (!volumes) { + throw new Error(`Could not find volume path in ${stdout}`); + } + const mountPath = volumes[0]!; + + try { + const fileNames = await readdir(mountPath); + const appName = fileNames.find(item => { + return typeof item === 'string' && item.endsWith('.app'); + }); + if (!appName) { + throw new Error(`Cannot find app in ${mountPath}`); + } + const mountedPath = path.join(mountPath!, appName); + + await exec(`cp -R "${mountedPath}" "${folderPath}"`); + } finally { + await exec(`hdiutil detach "${mountPath}" -quiet`); + } +} diff --git a/packages/browsers/src/httpUtil.ts b/packages/browsers/src/httpUtil.ts new file mode 100644 index 00000000..556b72ed --- /dev/null +++ b/packages/browsers/src/httpUtil.ts @@ -0,0 +1,136 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as http from 'http'; +import * as https from 'https'; +import {URL} from 'url'; +import createHttpsProxyAgent from 'https-proxy-agent'; +import {getProxyForUrl} from 'proxy-from-env'; +import {createWriteStream} from 'fs'; + +export function headHttpRequest(url: URL): Promise { + return new Promise(resolve => { + const request = httpRequest( + url, + 'HEAD', + response => { + resolve(response.statusCode === 200); + }, + false + ); + request.on('error', () => { + resolve(false); + }); + }); +} + +export function httpRequest( + url: URL, + method: string, + response: (x: http.IncomingMessage) => void, + keepAlive = true +): http.ClientRequest { + const options: http.RequestOptions = { + protocol: url.protocol, + hostname: url.hostname, + port: url.port, + path: url.pathname, + headers: keepAlive ? {Connection: 'keep-alive'} : undefined, + }; + + const proxyURL = getProxyForUrl(url.toString()); + if (proxyURL) { + const proxy = new URL(proxyURL); + if (proxy.protocol === 'http:') { + options.path = url.href; + options.hostname = proxy.hostname; + options.protocol = proxy.protocol; + } else { + options.agent = createHttpsProxyAgent({ + host: proxy.host, + path: proxy.pathname, + port: proxy.port, + secureProxy: proxy.protocol === 'https:', + headers: options.headers, + }); + } + } + + const requestCallback = (res: http.IncomingMessage): void => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + httpRequest(new URL(res.headers.location), method, response); + } else { + response(res); + } + }; + const request = + options.protocol === 'https:' + ? https.request(options, requestCallback) + : http.request(options, requestCallback); + request.end(); + return request; +} + +/** + * @internal + */ +export function downloadFile( + url: URL, + destinationPath: string, + progressCallback?: (downloadedBytes: number, totalBytes: number) => void +): Promise { + return new Promise((resolve, reject) => { + let downloadedBytes = 0; + let totalBytes = 0; + + function onData(chunk: string): void { + downloadedBytes += chunk.length; + progressCallback!(downloadedBytes, totalBytes); + } + + const request = httpRequest(url, 'GET', response => { + if (response.statusCode !== 200) { + const error = new Error( + `Download failed: server returned code ${response.statusCode}. URL: ${url}` + ); + // consume response data to free up memory + response.resume(); + reject(error); + return; + } + const file = createWriteStream(destinationPath); + file.on('finish', () => { + return resolve(); + }); + file.on('error', error => { + return reject(error); + }); + response.pipe(file); + totalBytes = parseInt(response.headers['content-length']!, 10); + if (progressCallback) { + response.on('data', onData); + } + }); + request.on('error', error => { + return reject(error); + }); + }); +} diff --git a/packages/browsers/src/tsconfig.cjs.json b/packages/browsers/src/tsconfig.cjs.json new file mode 100644 index 00000000..ef01b990 --- /dev/null +++ b/packages/browsers/src/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../lib/cjs" + } +} diff --git a/packages/browsers/src/tsconfig.esm.json b/packages/browsers/src/tsconfig.esm.json new file mode 100644 index 00000000..a824bc8c --- /dev/null +++ b/packages/browsers/src/tsconfig.esm.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../lib/esm" + } +} diff --git a/packages/browsers/test/src/chrome-data.spec.ts b/packages/browsers/test/src/chrome-data.spec.ts new file mode 100644 index 00000000..b18dedd6 --- /dev/null +++ b/packages/browsers/test/src/chrome-data.spec.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {resolveDownloadUrl} from '../../lib/cjs/browsers/chrome.js'; +import {BrowserPlatform} from '../../lib/cjs/browsers/browsers.js'; +import assert from 'assert'; + +describe('Chrome', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/1083080/chrome-mac.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1083080/chrome-mac.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/1083080/chrome-win.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/1083080/chrome-win.zip' + ); + }); +}); diff --git a/packages/browsers/test/src/fetch.spec.ts b/packages/browsers/test/src/fetch.spec.ts new file mode 100644 index 00000000..3ee0e417 --- /dev/null +++ b/packages/browsers/test/src/fetch.spec.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {fetch, canFetch} from '../../lib/cjs/fetch.js'; +import {Browser, BrowserPlatform} from '../../lib/cjs/browsers/browsers.js'; + +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import assert from 'assert'; + +/** + * Tests in this spec use real download URLs and unpack live browser archives + * so it requires the network access. + */ +describe('fetch', () => { + let tmpDir = '/tmp/puppeteer-browsers-test'; + const testChromeRevision = '1083080'; + const testFirefoxRevision = '111.0a1'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, {recursive: true}); + }); + + it('should check if a revision can be downloaded', async () => { + assert.ok( + await canFetch({ + outputDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + revision: testChromeRevision, + }) + ); + }); + + it('should report if a revision is not downloadable', async () => { + assert.strictEqual( + await canFetch({ + outputDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + revision: 'unknown', + }), + false + ); + }); + + it('should download a revision that is a zip archive', async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + `${BrowserPlatform.LINUX}-${testChromeRevision}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + let browser = await fetch({ + outputDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + revision: testChromeRevision, + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + // Second iteration should be no-op. + browser = await fetch({ + outputDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + revision: testChromeRevision, + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + }); + + it('should download a revision that is a bzip2 archive', async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + `${BrowserPlatform.LINUX}-${testFirefoxRevision}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + const browser = await fetch({ + outputDir: tmpDir, + browser: Browser.FIREFOX, + platform: BrowserPlatform.LINUX, + revision: testFirefoxRevision, + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + }); + + // Fetch relies on the `hdiutil` utility on MacOS. + // The utility is not available on other platforms. + (os.platform() === 'darwin' ? it : it.skip)( + 'should download a revision that is a dmg archive', + async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + `${BrowserPlatform.MAC}-${testFirefoxRevision}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + const browser = await fetch({ + outputDir: tmpDir, + browser: Browser.FIREFOX, + platform: BrowserPlatform.MAC, + revision: testFirefoxRevision, + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + } + ); +}); diff --git a/packages/browsers/test/src/firefox-data.spec.ts b/packages/browsers/test/src/firefox-data.spec.ts new file mode 100644 index 00000000..8822aece --- /dev/null +++ b/packages/browsers/test/src/firefox-data.spec.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {resolveDownloadUrl} from '../../lib/cjs/browsers/firefox.js'; +import {BrowserPlatform} from '../../lib/cjs/browsers/browsers.js'; +import assert from 'assert'; + +describe('Firefox', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win64.zip' + ); + }); +}); diff --git a/packages/browsers/test/src/tsconfig.json b/packages/browsers/test/src/tsconfig.json new file mode 100644 index 00000000..63dd3f1e --- /dev/null +++ b/packages/browsers/test/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../build" + }, + "references": [{"path": "../../tsconfig.json"}] +} diff --git a/packages/browsers/tsconfig.json b/packages/browsers/tsconfig.json new file mode 100644 index 00000000..a219f8b7 --- /dev/null +++ b/packages/browsers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "references": [ + {"path": "src/tsconfig.esm.json"}, + {"path": "src/tsconfig.cjs.json"} + ] +}