feat: support local aliases when launching a browser (#11947)

This commit is contained in:
Alex Rudenko 2024-02-20 19:09:36 +01:00 committed by GitHub
parent bf71b34fca
commit 561e4cd6ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 222 additions and 15 deletions

View File

@ -23,3 +23,10 @@ The constructor for this class is marked as internal. Third-party code should no
| executablePath | <code>readonly</code> | string | | | executablePath | <code>readonly</code> | string | |
| path | <code>readonly</code> | string | Path to the root of the installation folder. Use [computeExecutablePath()](./browsers.computeexecutablepath.md) to get the path to the executable binary. | | path | <code>readonly</code> | string | Path to the root of the installation folder. Use [computeExecutablePath()](./browsers.computeexecutablepath.md) to get the path to the executable binary. |
| platform | | [BrowserPlatform](./browsers.browserplatform.md) | | | platform | | [BrowserPlatform](./browsers.browserplatform.md) | |
## Methods
| Method | Modifiers | Description |
| ----------------------------------------------------------------------- | --------- | ----------- |
| [readMetadata()](./browsers.installedbrowser.readmetadata.md) | | |
| [writeMetadata(metadata)](./browsers.installedbrowser.writemetadata.md) | | |

View File

@ -0,0 +1,17 @@
---
sidebar_label: InstalledBrowser.readMetadata
---
# InstalledBrowser.readMetadata() method
#### Signature:
```typescript
class InstalledBrowser {
readMetadata(): Metadata;
}
```
**Returns:**
Metadata

View File

@ -0,0 +1,23 @@
---
sidebar_label: InstalledBrowser.writeMetadata
---
# InstalledBrowser.writeMetadata() method
#### Signature:
```typescript
class InstalledBrowser {
writeMetadata(metadata: Metadata): void;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | -------- | ----------- |
| metadata | Metadata | |
**Returns:**
void

View File

@ -12,12 +12,13 @@ export interface InstallOptions
## Properties ## Properties
| Property | Modifiers | Type | Description | Default | | Property | Modifiers | Type | Description | Default |
| ------------------------ | --------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------ | --------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| baseUrl | <code>optional</code> | string | Determines the host that will be used for downloading. | <p>Either</p><p>- https://storage.googleapis.com/chrome-for-testing-public or - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central</p> | | baseUrl | <code>optional</code> | string | Determines the host that will be used for downloading. | <p>Either</p><p>- https://storage.googleapis.com/chrome-for-testing-public or - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central</p> |
| browser | | [Browser](./browsers.browser.md) | Determines which browser to install. | | | browser | | [Browser](./browsers.browser.md) | Determines which browser to install. | |
| buildId | | string | Determines which buildId to download. BuildId should uniquely identify binaries and they are used for caching. | | | buildId | | string | Determines which buildId to download. BuildId should uniquely identify binaries and they are used for caching. | |
| cacheDir | | string | Determines the path to download browsers to. | | | buildIdAlias | <code>optional</code> | string | An alias for the provided <code>buildId</code>. It will be used to maintain local metadata to support aliases in the <code>launch</code> command. | |
| downloadProgressCallback | <code>optional</code> | (downloadedBytes: number, totalBytes: number) =&gt; void | Provides information about the progress of the download. | | | cacheDir | | string | Determines the path to download browsers to. | |
| platform | <code>optional</code> | [BrowserPlatform](./browsers.browserplatform.md) | Determines which platform the browser will be suited for. | **Auto-detected.** | | downloadProgressCallback | <code>optional</code> | (downloadedBytes: number, totalBytes: number) =&gt; void | Provides information about the progress of the download. | |
| unpack | <code>optional</code> | boolean | Whether to unpack and install browser archives. | <code>true</code> | | platform | <code>optional</code> | [BrowserPlatform](./browsers.browserplatform.md) | Determines which platform the browser will be suited for. | **Auto-detected.** |
| unpack | <code>optional</code> | boolean | Whether to unpack and install browser archives. | <code>true</code> |

View File

@ -238,6 +238,7 @@ export class CLI {
} }
args.browser.buildId = pinnedVersion; args.browser.buildId = pinnedVersion;
} }
const originalBuildId = args.browser.buildId;
args.browser.buildId = await resolveBuildId( args.browser.buildId = await resolveBuildId(
args.browser.name, args.browser.name,
args.platform, args.platform,
@ -253,6 +254,10 @@ export class CLI {
args.browser.buildId args.browser.buildId
), ),
baseUrl: args.baseUrl, baseUrl: args.baseUrl,
buildIdAlias:
originalBuildId !== args.browser.buildId
? originalBuildId
: undefined,
}); });
console.log( console.log(
`${args.browser.name}@${ `${args.browser.name}@${

View File

@ -8,13 +8,18 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import debug from 'debug';
import { import {
Browser, Browser,
type BrowserPlatform, type BrowserPlatform,
executablePathByBrowser, executablePathByBrowser,
getVersionComparator,
} from './browser-data/browser-data.js'; } from './browser-data/browser-data.js';
import {detectBrowserPlatform} from './detectPlatform.js'; import {detectBrowserPlatform} from './detectPlatform.js';
const debugCache = debug('puppeteer:browsers:cache');
/** /**
* @public * @public
*/ */
@ -57,6 +62,14 @@ export class InstalledBrowser {
this.buildId this.buildId
); );
} }
readMetadata(): Metadata {
return this.#cache.readMetadata(this.browser);
}
writeMetadata(metadata: Metadata): void {
this.#cache.writeMetadata(this.browser, metadata);
}
} }
/** /**
@ -80,6 +93,11 @@ export interface ComputeExecutablePathOptions {
buildId: string; buildId: string;
} }
export interface Metadata {
// Maps an alias (canary/latest/dev/etc.) to a buildId.
aliases: Record<string, string>;
}
/** /**
* The cache used by Puppeteer relies on the following structure: * The cache used by Puppeteer relies on the following structure:
* *
@ -112,6 +130,39 @@ export class Cache {
return path.join(this.#rootDir, browser); return path.join(this.#rootDir, browser);
} }
metadataFile(browser: Browser): string {
return path.join(this.browserRoot(browser), '.metadata');
}
readMetadata(browser: Browser): Metadata {
const metatadaPath = this.metadataFile(browser);
if (!fs.existsSync(metatadaPath)) {
return {aliases: {}};
}
// TODO: add type-safe parsing.
const data = JSON.parse(fs.readFileSync(metatadaPath, 'utf8'));
if (typeof data !== 'object') {
throw new Error('.metadata is not an object');
}
return data;
}
writeMetadata(browser: Browser, metadata: Metadata): void {
const metatadaPath = this.metadataFile(browser);
fs.mkdirSync(path.dirname(metatadaPath), {recursive: true});
fs.writeFileSync(metatadaPath, JSON.stringify(metadata, null, 2));
}
resolveAlias(browser: Browser, alias: string): string | undefined {
const metadata = this.readMetadata(browser);
if (alias === 'latest') {
return Object.values(metadata.aliases || {})
.sort(getVersionComparator(browser))
.at(-1);
}
return metadata.aliases[alias];
}
installationDir( installationDir(
browser: Browser, browser: Browser,
platform: BrowserPlatform, platform: BrowserPlatform,
@ -134,6 +185,12 @@ export class Cache {
platform: BrowserPlatform, platform: BrowserPlatform,
buildId: string buildId: string
): void { ): void {
const metadata = this.readMetadata(browser);
for (const alias of Object.keys(metadata.aliases)) {
if (metadata.aliases[alias] === buildId) {
delete metadata.aliases[alias];
}
}
fs.rmSync(this.installationDir(browser, platform, buildId), { fs.rmSync(this.installationDir(browser, platform, buildId), {
force: true, force: true,
recursive: true, recursive: true,
@ -180,6 +237,12 @@ export class Cache {
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
); );
} }
try {
options.buildId =
this.resolveAlias(options.browser, options.buildId) ?? options.buildId;
} catch {
debugCache('could not read .metadata file for the browser');
}
const installationDir = this.installationDir( const installationDir = this.installationDir(
options.browser, options.browser,
options.platform, options.platform,

View File

@ -62,6 +62,13 @@ export interface InstallOptions {
* binaries and they are used for caching. * binaries and they are used for caching.
*/ */
buildId: string; buildId: string;
/**
* An alias for the provided `buildId`. It will be used to maintain local
* metadata to support aliases in the `launch` command.
*
* @example 'canary'
*/
buildIdAlias?: string;
/** /**
* Provides information about the progress of the download. * Provides information about the progress of the download.
*/ */
@ -233,17 +240,23 @@ async function installUrl(
} finally { } finally {
debugTimeEnd('extract'); debugTimeEnd('extract');
} }
const installedBrowser = new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform
);
if (options.buildIdAlias) {
const metadata = installedBrowser.readMetadata();
metadata.aliases[options.buildIdAlias] = options.buildId;
installedBrowser.writeMetadata(metadata);
}
return installedBrowser;
} finally { } finally {
if (existsSync(archivePath)) { if (existsSync(archivePath)) {
await unlink(archivePath); await unlink(archivePath);
} }
} }
return new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform
);
} }
/** /**

View File

@ -0,0 +1,72 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'assert';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {Browser, Cache} from '../../lib/cjs/main.js';
describe('Cache', () => {
let tmpDir = '/tmp/puppeteer-browsers-test';
let cache: Cache;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
cache = new Cache(tmpDir);
});
afterEach(() => {
cache.clear();
});
it('return empty metadata if .metadata file does not exist', async function () {
assert.deepStrictEqual(cache.readMetadata(Browser.CHROME), {
aliases: {},
});
});
it('throw an error if .metadata is malformed', async function () {
// @ts-expect-error wrong type on purpose;
cache.writeMetadata(Browser.CHROME, 'metadata');
assert.throws(() => {
return cache.readMetadata(Browser.CHROME);
}, new Error(`.metadata is not an object`));
});
it('writes and reads .metadata', async function () {
cache.writeMetadata(Browser.CHROME, {
aliases: {
canary: '123.0.0.0',
},
});
assert.deepStrictEqual(cache.readMetadata(Browser.CHROME), {
aliases: {
canary: '123.0.0.0',
},
});
assert.deepStrictEqual(
cache.resolveAlias(Browser.CHROME, 'canary'),
'123.0.0.0'
);
});
it('resolves latest', async function () {
cache.writeMetadata(Browser.CHROME, {
aliases: {
canary: '115.0.5789',
stable: '114.0.5789',
},
});
assert.deepStrictEqual(
cache.resolveAlias(Browser.CHROME, 'latest'),
'115.0.5789'
);
});
});

View File

@ -74,6 +74,8 @@ export async function downloadBrowser(): Promise<void> {
buildId, buildId,
downloadProgressCallback: makeProgressCallback(browser, buildId), downloadProgressCallback: makeProgressCallback(browser, buildId),
baseUrl: downloadBaseUrl, baseUrl: downloadBaseUrl,
buildIdAlias:
buildId !== unresolvedBuildId ? unresolvedBuildId : undefined,
}) })
.then(result => { .then(result => {
logPolitely( logPolitely(
@ -112,6 +114,10 @@ export async function downloadBrowser(): Promise<void> {
shellBuildId shellBuildId
), ),
baseUrl: downloadBaseUrl, baseUrl: downloadBaseUrl,
buildIdAlias:
shellBuildId !== unresolvedShellBuildId
? unresolvedShellBuildId
: undefined,
}) })
.then(result => { .then(result => {
logPolitely( logPolitely(