Skip to content

Commit

Permalink
feat: support launch aliases
Browse files Browse the repository at this point in the history
With this PR, @puppeteer/browsers starts maintaining a local
mapping from an alias to the locally installed version.
This allows using aliases such as `canary`/`latest` when launching
the browser.

Note that the launch command would only consult metadata files
and won't attempt to re-check aliases by contacting the server.
  • Loading branch information
OrKoN committed Feb 19, 2024
1 parent 84ad6de commit 15c77f8
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 21 deletions.
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
30 changes: 30 additions & 0 deletions docs/browsers-api/browsers.resolvelocalalias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
sidebar_label: resolveLocalAlias
---

# resolveLocalAlias() function

Resolves an alias (canary/dev/latest/etc) to a buildId based on the `.metadata` file in the browser folder.

#### Signature:

```typescript
export declare function resolveLocalAlias(
options: {
browser: Browser;
cacheDir: string;
},
alias: string
): string | undefined;
```

## Parameters

| Parameter | Type | Description |
| --------- | -------------------------------------------------------------------------- | ----------- |
| options | &#123; browser: [Browser](./browsers.browser.md); cacheDir: string; &#125; | |
| alias | string | |

**Returns:**

string \| undefined
29 changes: 15 additions & 14 deletions docs/browsers-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,21 @@ The programmatic API allows installing and launching browsers from your code. Se

## Functions

| Function | Description |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| [canDownload(options)](./browsers.candownload.md) | |
| [computeExecutablePath(options)](./browsers.computeexecutablepath.md) | |
| [computeSystemExecutablePath(options)](./browsers.computesystemexecutablepath.md) | |
| [createProfile(browser, opts)](./browsers.createprofile.md) | |
| [detectBrowserPlatform()](./browsers.detectbrowserplatform.md) | |
| [getInstalledBrowsers(options)](./browsers.getinstalledbrowsers.md) | Returns metadata about browsers installed in the cache directory. |
| [install(options)](./browsers.install.md) | |
| [install(options)](./browsers.install_1.md) | |
| [launch(opts)](./browsers.launch.md) | |
| [makeProgressCallback(browser, buildId)](./browsers.makeprogresscallback.md) | |
| [resolveBuildId(browser, platform, tag)](./browsers.resolvebuildid.md) | |
| [uninstall(options)](./browsers.uninstall.md) | |
| Function | Description |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [canDownload(options)](./browsers.candownload.md) | |
| [computeExecutablePath(options)](./browsers.computeexecutablepath.md) | |
| [computeSystemExecutablePath(options)](./browsers.computesystemexecutablepath.md) | |
| [createProfile(browser, opts)](./browsers.createprofile.md) | |
| [detectBrowserPlatform()](./browsers.detectbrowserplatform.md) | |
| [getInstalledBrowsers(options)](./browsers.getinstalledbrowsers.md) | Returns metadata about browsers installed in the cache directory. |
| [install(options)](./browsers.install.md) | |
| [install(options)](./browsers.install_1.md) | |
| [launch(opts)](./browsers.launch.md) | |
| [makeProgressCallback(browser, buildId)](./browsers.makeprogresscallback.md) | |
| [resolveBuildId(browser, platform, tag)](./browsers.resolvebuildid.md) | |
| [resolveLocalAlias(options, alias)](./browsers.resolvelocalalias.md) | Resolves an alias (canary/dev/latest/etc) to a buildId based on the <code>.metadata</code> file in the browser folder. |
| [uninstall(options)](./browsers.uninstall.md) | |

## Interfaces

Expand Down
22 changes: 21 additions & 1 deletion packages/browsers/src/CLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,13 @@ export class CLI {
}
args.browser.buildId = pinnedVersion;
}
const originalBuildId = args.browser.buildId;
args.browser.buildId = await resolveBuildId(
args.browser.name,
args.platform,
args.browser.buildId
);
await install({
const installedBrowser = await install({
browser: args.browser.name,
buildId: args.browser.buildId,
platform: args.platform,
Expand All @@ -254,6 +255,13 @@ export class CLI {
),
baseUrl: args.baseUrl,
});
if (originalBuildId !== args.browser.buildId) {
const alias = originalBuildId;
const metadata = installedBrowser.readMetadata();
metadata.aliases[alias] = args.browser.buildId;
metadata.aliases['latest'] = args.browser.buildId;
installedBrowser.writeMetadata(metadata);
}
console.log(
`${args.browser.name}@${
args.browser.buildId
Expand Down Expand Up @@ -302,6 +310,18 @@ export class CLI {
},
async argv => {
const args = argv as unknown as LaunchArgs;
const cacheDir = args.path ?? this.#cachePath;
const cache = new Cache(cacheDir);

if (!args.system) {
try {
args.browser.buildId =
cache.resolveAlias(args.browser.name, args.browser.buildId) ??
args.browser.buildId;
} catch {
console.warn('could not read .metadata file for the browser');
}
}
const executablePath = args.system
? computeSystemExecutablePath({
browser: args.browser.name,
Expand Down
47 changes: 47 additions & 0 deletions packages/browsers/src/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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 +88,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 +125,34 @@ 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);
return metadata.aliases[alias];
}

installationDir(
browser: Browser,
platform: BrowserPlatform,
Expand All @@ -134,6 +175,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
14 changes: 14 additions & 0 deletions packages/browsers/src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,20 @@ export async function getInstalledBrowsers(
return new Cache(options.cacheDir).getInstalledBrowsers();
}

/**
* Resolves an alias (canary/dev/latest/etc) to a buildId based on the
* `.metadata` file in the browser folder.
*/
export function resolveLocalAlias(
options: {
browser: Browser;
cacheDir: string;
},
alias: string
): string | undefined {
return new Cache(options.cacheDir).resolveAlias(options.browser, alias);
}

/**
* @public
*/
Expand Down
1 change: 1 addition & 0 deletions packages/browsers/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
getInstalledBrowsers,
canDownload,
uninstall,
resolveLocalAlias,
} from './install.js';
export {detectBrowserPlatform} from './detectPlatform.js';
export type {ProfileOptions} from './browser-data/browser-data.js';
Expand Down
58 changes: 58 additions & 0 deletions packages/browsers/test/src/Cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @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'
);
});
});
30 changes: 24 additions & 6 deletions packages/puppeteer/src/node/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
resolveBuildId,
makeProgressCallback,
detectBrowserPlatform,
resolveLocalAlias,
} from '@puppeteer/browsers';
import type {Product} from 'puppeteer-core';
import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';
Expand Down Expand Up @@ -61,11 +62,17 @@ export async function downloadBrowser(): Promise<void> {
if (configuration.skipChromeDownload) {
logPolitely('**INFO** Skipping Chrome download as instructed.');
} else {
const buildId = await resolveBuildId(
browser,
platform,
unresolvedBuildId
);
let buildId = await resolveBuildId(browser, platform, unresolvedBuildId);
if (unresolvedBuildId !== unresolvedShellBuildId) {
buildId =
resolveLocalAlias(
{
browser,
cacheDir,
},
unresolvedShellBuildId
) ?? buildId;
}
installationJobs.push(
install({
browser,
Expand Down Expand Up @@ -95,12 +102,23 @@ export async function downloadBrowser(): Promise<void> {
if (configuration.skipChromeHeadlessShellDownload) {
logPolitely('**INFO** Skipping Chrome download as instructed.');
} else {
const shellBuildId = await resolveBuildId(
let shellBuildId = await resolveBuildId(
browser,
platform,
unresolvedShellBuildId
);

if (shellBuildId !== unresolvedShellBuildId) {
shellBuildId =
resolveLocalAlias(
{
browser,
cacheDir,
},
unresolvedShellBuildId
) ?? shellBuildId;
}

installationJobs.push(
install({
browser: Browser.CHROMEHEADLESSSHELL,
Expand Down

0 comments on commit 15c77f8

Please sign in to comment.