Skip to content

Commit

Permalink
feat: support local aliases when launching a browser (#11947)
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed Feb 20, 2024
1 parent bf71b34 commit 561e4cd
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 15 deletions.
7 changes: 7 additions & 0 deletions docs/browsers-api/browsers.installedbrowser.md
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
@@ -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
@@ -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
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
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
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
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
@@ -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
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

0 comments on commit 561e4cd

Please sign in to comment.