chore: introduce @puppeteer/browsers with a fetch method implementation (#9647)
Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
This commit is contained in:
parent
6ada628f39
commit
b50e43bc17
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@ -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'
|
||||
|
57
package-lock.json
generated
57
package-lock.json
generated
@ -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": {
|
||||
|
6
packages/browsers/.mocharc.cjs
Normal file
6
packages/browsers/.mocharc.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
logLevel: 'debug',
|
||||
spec: 'test/build/**/*.spec.js',
|
||||
exit: !!process.env.CI,
|
||||
reporter: 'spec',
|
||||
};
|
3
packages/browsers/README.md
Normal file
3
packages/browsers/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @puppeteer/browsers
|
||||
|
||||
TODO
|
81
packages/browsers/package.json
Normal file
81
packages/browsers/package.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
26
packages/browsers/src/browsers/browsers.ts
Normal file
26
packages/browsers/src/browsers/browsers.ts
Normal file
@ -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};
|
57
packages/browsers/src/browsers/chrome.ts
Normal file
57
packages/browsers/src/browsers/chrome.ts
Normal file
@ -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`;
|
||||
}
|
39
packages/browsers/src/browsers/firefox.ts
Normal file
39
packages/browsers/src/browsers/firefox.ts
Normal file
@ -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)}`;
|
||||
}
|
43
packages/browsers/src/browsers/types.ts
Normal file
43
packages/browsers/src/browsers/types.ts
Normal file
@ -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,
|
||||
};
|
19
packages/browsers/src/debug.ts
Normal file
19
packages/browsers/src/debug.ts
Normal file
@ -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};
|
171
packages/browsers/src/fetch.ts
Normal file
171
packages/browsers/src/fetch.ts
Normal file
@ -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<InstalledBrowser> {
|
||||
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<boolean> {
|
||||
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));
|
||||
}
|
88
packages/browsers/src/fileUtil.ts
Normal file
88
packages/browsers/src/fileUtil.ts
Normal file
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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`);
|
||||
}
|
||||
}
|
136
packages/browsers/src/httpUtil.ts
Normal file
136
packages/browsers/src/httpUtil.ts
Normal file
@ -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<boolean> {
|
||||
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<void> {
|
||||
return new Promise<void>((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);
|
||||
});
|
||||
});
|
||||
}
|
7
packages/browsers/src/tsconfig.cjs.json
Normal file
7
packages/browsers/src/tsconfig.cjs.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "../lib/cjs"
|
||||
}
|
||||
}
|
6
packages/browsers/src/tsconfig.esm.json
Normal file
6
packages/browsers/src/tsconfig.esm.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../lib/esm"
|
||||
}
|
||||
}
|
44
packages/browsers/test/src/chrome-data.spec.ts
Normal file
44
packages/browsers/test/src/chrome-data.spec.ts
Normal file
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
129
packages/browsers/test/src/fetch.spec.ts
Normal file
129
packages/browsers/test/src/fetch.spec.ts
Normal file
@ -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));
|
||||
}
|
||||
);
|
||||
});
|
44
packages/browsers/test/src/firefox-data.spec.ts
Normal file
44
packages/browsers/test/src/firefox-data.spec.ts
Normal file
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
8
packages/browsers/test/src/tsconfig.json
Normal file
8
packages/browsers/test/src/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "../build"
|
||||
},
|
||||
"references": [{"path": "../../tsconfig.json"}]
|
||||
}
|
8
packages/browsers/tsconfig.json
Normal file
8
packages/browsers/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{"path": "src/tsconfig.esm.json"},
|
||||
{"path": "src/tsconfig.cjs.json"}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user