Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support local aliases when launching a browser #11947

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/browsers-api/browsers.installedbrowser.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ The constructor for this class is marked as internal. Third-party code should no
| 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. |
| platform | | [BrowserPlatform](./browsers.browserplatform.md) | |

## Methods

| Method | Modifiers | Description |
| ----------------------------------------------------------------------- | --------- | ----------- |
| [readMetadata()](./browsers.installedbrowser.readmetadata.md) | | |
| [writeMetadata(metadata)](./browsers.installedbrowser.writemetadata.md) | | |
17 changes: 17 additions & 0 deletions docs/browsers-api/browsers.installedbrowser.readmetadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
sidebar_label: InstalledBrowser.readMetadata
---

# InstalledBrowser.readMetadata() method

#### Signature:

```typescript
class InstalledBrowser {
readMetadata(): Metadata;
}
```

**Returns:**

Metadata
23 changes: 23 additions & 0 deletions docs/browsers-api/browsers.installedbrowser.writemetadata.md
Original file line number Diff line number Diff line change
@@ -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
19 changes: 10 additions & 9 deletions docs/browsers-api/browsers.installoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ export interface InstallOptions

## Properties

| 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> |
| 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. | |
| cacheDir | | string | Determines the path to download browsers to. | |
| downloadProgressCallback | <code>optional</code> | (downloadedBytes: number, totalBytes: number) =&gt; void | Provides information about the progress of the download. | |
| 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> |
| 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> |
| 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. | |
| 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. | |
| cacheDir | | string | Determines the path to download browsers to. | |
| downloadProgressCallback | <code>optional</code> | (downloadedBytes: number, totalBytes: number) =&gt; void | Provides information about the progress of the download. | |
| 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> |
5 changes: 5 additions & 0 deletions packages/browsers/src/CLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export class CLI {
}
args.browser.buildId = pinnedVersion;
}
const originalBuildId = args.browser.buildId;
args.browser.buildId = await resolveBuildId(
args.browser.name,
args.platform,
Expand All @@ -253,6 +254,10 @@ export class CLI {
args.browser.buildId
),
baseUrl: args.baseUrl,
buildIdAlias:
originalBuildId !== args.browser.buildId
? originalBuildId
: undefined,
});
console.log(
`${args.browser.name}@${
Expand Down
63 changes: 63 additions & 0 deletions packages/browsers/src/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import fs from 'fs';
import os from 'os';
import path from 'path';

import debug from 'debug';

import {
Browser,
type BrowserPlatform,
executablePathByBrowser,
getVersionComparator,
} from './browser-data/browser-data.js';
import {detectBrowserPlatform} from './detectPlatform.js';

const debugCache = debug('puppeteer:browsers:cache');

/**
* @public
*/
Expand Down Expand Up @@ -57,6 +62,14 @@ export class InstalledBrowser {
this.buildId
);
}

readMetadata(): Metadata {
return this.#cache.readMetadata(this.browser);
}

writeMetadata(metadata: Metadata): void {
this.#cache.writeMetadata(this.browser, metadata);
}
}

/**
Expand All @@ -80,6 +93,11 @@ export interface ComputeExecutablePathOptions {
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:
*
Expand Down Expand Up @@ -112,6 +130,39 @@ export class Cache {
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(
browser: Browser,
platform: BrowserPlatform,
Expand All @@ -134,6 +185,12 @@ export class Cache {
platform: BrowserPlatform,
buildId: string
): 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), {
force: true,
recursive: true,
Expand Down Expand Up @@ -180,6 +237,12 @@ export class Cache {
`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(
options.browser,
options.platform,
Expand Down
25 changes: 19 additions & 6 deletions packages/browsers/src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export interface InstallOptions {
* binaries and they are used for caching.
*/
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.
*/
Expand Down Expand Up @@ -233,17 +240,23 @@ async function installUrl(
} finally {
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 {
if (existsSync(archivePath)) {
await unlink(archivePath);
}
}
return new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform
);
}

/**
Expand Down
72 changes: 72 additions & 0 deletions packages/browsers/test/src/Cache.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
6 changes: 6 additions & 0 deletions packages/puppeteer/src/node/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export async function downloadBrowser(): Promise<void> {
buildId,
downloadProgressCallback: makeProgressCallback(browser, buildId),
baseUrl: downloadBaseUrl,
buildIdAlias:
buildId !== unresolvedBuildId ? unresolvedBuildId : undefined,
})
.then(result => {
logPolitely(
Expand Down Expand Up @@ -112,6 +114,10 @@ export async function downloadBrowser(): Promise<void> {
shellBuildId
),
baseUrl: downloadBaseUrl,
buildIdAlias:
shellBuildId !== unresolvedShellBuildId
? unresolvedShellBuildId
: undefined,
})
.then(result => {
logPolitely(
Expand Down