Skip to content

Commit

Permalink
Merge pull request #237 from reg-viz/separate-chromium
Browse files Browse the repository at this point in the history
Separate puppeteer dependencies
  • Loading branch information
Quramy authored Sep 15, 2020
2 parents 667ee2b + fb5a4e8 commit 3c492d1
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 68 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ It is primarily responsible for image generation necessary for Visual Testing su
- [Tips](#tips)
- [Run with Docker](#run-with-docker)
- [Full control the screenshot timing](#full-control-the-screenshot-timing)
- [Chromium version](#chromium-version)
- [Storybook compatibility](#storybook-compatibility)
- [Storybook versions](#storybook-versions)
- [UI frameworks](#ui-frameworks)
Expand All @@ -61,6 +62,14 @@ It is primarily responsible for image generation necessary for Visual Testing su
$ npm install storycap
```

Or

```sh
$ npm install storycap puppeteer
```

Installing puppeteer is optional. See [Chromium version](#chromium-version) to get more detail.

## Getting Started

Storycap runs with 2 modes. One is "simple" and another is "managed".
Expand Down Expand Up @@ -332,6 +341,9 @@ Options:
--reloadAfterChangeViewport Whether to reload after viewport changed. [boolean] [default: false]
--stateChangeDelay Delay time [msec] after changing element's state. [number] [default: 0]
--listDevices List available device descriptors. [boolean] [default: false]
-C, --chromiumChannel Channel to search local Chromium. One of "puppeteer", "canary", "stable", "*"
[string] [default: "*"]
--chromiumPath Executable Chromium path. [string] [default: ""]
--puppeteerLaunchConfig JSON string of launch config for Puppeteer.
[string] [default: "{ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] }"]
Expand Down Expand Up @@ -473,6 +485,16 @@ addParameters({
});
```

## Chromium version

Since v3.0.0, Storycap does not use Puppeteer directly. Instead, Storycap searches Chromium binary in the following order:

1. Installed Puppeteer package (if you installed explicitly)
1. Canary Chrome installed locally
1. Stable Chrome installed locally

You can change search channel with `--chromiumChannel` option or set executable Chromium file path with `--chromiumPath` option.

## Storybook compatibility

### Storybook versions
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"packages": ["packages/*"],
"version": "2.3.7",
"version": "3.0.0-alpha.1",
"useWorkspaces": true,
"npmClient": "yarn"
}
10 changes: 6 additions & 4 deletions packages/storycap/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "storycap",
"version": "2.3.7",
"version": "3.0.0-alpha.1",
"description": "A Storybook addon, Save the screenshot image of your stories! via puppeteer.",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -49,8 +49,10 @@
],
"devDependencies": {
"@types/jest": "26.0.13",
"@types/puppeteer": "^3.0.2",
"jest": "26.4.2",
"minimist": "1.2.5",
"puppeteer": "^5.3.0",
"ts-jest": "26.3.0",
"typedoc": "0.19.1",
"typescript": "4.0.2"
Expand All @@ -59,17 +61,17 @@
"@types/minimatch": "^3.0.3",
"@types/mkdirp": "^1.0.0",
"@types/node": "^12.6.8",
"@types/puppeteer": "^3.0.0",
"@types/puppeteer-core": "^2.0.0",
"@types/rimraf": "^3.0.0",
"@types/wait-on": "^4.0.0",
"@types/yargs": "^15.0.0",
"core-js": "^3.2.1",
"minimatch": "^3.0.4",
"mkdirp": "^1.0.0",
"puppeteer": "^1.19.0",
"puppeteer-core": "5.3.0",
"rimraf": "^3.0.0",
"sanitize-filename": "^1.6.3",
"storycrawler": "^2.3.7",
"storycrawler": "^3.0.0-alpha.1",
"yargs": "^16.0.0"
},
"jest": {
Expand Down
6 changes: 3 additions & 3 deletions packages/storycap/src/node/capturing-browser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import path from 'path';
import { Viewport } from 'puppeteer';
import type { Viewport } from 'puppeteer-core';
import { Story, StorybookConnection, StoryPreviewBrowser, MetricsWatcher, ResourceWatcher, sleep } from 'storycrawler';

import { MainOptions, RunMode } from './types';
Expand All @@ -13,7 +13,7 @@ import {
pickupWithVariantKey,
InvalidVariantKeysReason,
} from '../shared/screenshot-options-helper';
const dd = require('puppeteer/DeviceDescriptors') as { name: string; viewport: Viewport }[];
import { getDeviceDescriptors } from './devices';

/**
*
Expand Down Expand Up @@ -213,7 +213,7 @@ export class CapturingBrowser extends StoryPreviewBrowser {
nextViewport = { width: +w, height: +h };
} else {
// Handle as Puppeteer device descriptor.
const hit = dd.find(d => d.name === opt.viewport);
const hit = getDeviceDescriptors().find(d => d.name === opt.viewport);
if (!hit) {
this.opt.logger.warn(
`Skip screenshot for ${this.opt.logger.color.yellow(
Expand Down
21 changes: 18 additions & 3 deletions packages/storycap/src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import { time } from 'storycrawler';
import { main } from './main';
import { MainOptions } from './types';
import { MainOptions, ChromeChannel } from './types';
import yargs from 'yargs';
import { Logger } from './logger';
import { getDeviceDescriptors } from './devices';

function showDevices(logger: Logger) {
const dd = require('puppeteer/DeviceDescriptors') as { name: string; viewport: any }[];
dd.map(device => logger.log(device.name, JSON.stringify(device.viewport)));
getDeviceDescriptors().map(device => logger.log(device.name, JSON.stringify(device.viewport)));
}

function createOptions(): MainOptions {
Expand Down Expand Up @@ -69,6 +69,17 @@ function createOptions(): MainOptions {
default: false,
description: 'List available device descriptors.',
})
.option('chromiumChannel', {
alias: 'C',
string: true,
default: '*',
description: 'Channel to search local Chromium. One of "puppeteer", "canary", "stable", "*"',
})
.option('chromiumPath', {
string: true,
default: '',
description: 'Executable Chromium path.',
})
.option('puppeteerLaunchConfig', {
string: true,
default: '{ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] }',
Expand Down Expand Up @@ -108,6 +119,8 @@ function createOptions(): MainOptions {
disableCssAnimation,
disableWaitAssets,
listDevices,
chromiumChannel,
chromiumPath,
puppeteerLaunchConfig: puppeteerLaunchConfigString,
} = setting.argv;

Expand Down Expand Up @@ -150,6 +163,8 @@ function createOptions(): MainOptions {
stateChangeDelay,
disableCssAnimation,
disableWaitAssets,
chromiumChannel: chromiumChannel as ChromeChannel,
chromiumPath,
launchOptions: puppeteerLaunchConfig,
logger,
} as MainOptions;
Expand Down
11 changes: 11 additions & 0 deletions packages/storycap/src/node/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Viewport } from 'puppeteer-core';

/**
*
* @returns Puppeteer device discriptors
*
*/
export function getDeviceDescriptors() {
const dd = require('puppeteer-core').devices as Record<string, { name: string; viewport: Viewport }>;
return Object.values(dd);
}
200 changes: 200 additions & 0 deletions packages/storycap/src/node/find-chrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import fs from 'fs';
import path from 'path';
import { execSync, execFileSync } from 'child_process';
import { ChromeChannel } from './types';

// const puppeteer = require('puppeteer-core');

const newLineRegex = /\r?\n/;

function canAccess(file: string) {
if (!file) return false;

try {
fs.accessSync(file);
return true;
} catch (e) {
return false;
}
}

function findChromeExecutables(folder: string) {
const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space
const chromeExecRegex = '^Exec=/.*/(google-chrome|chrome|chromium)-.*';

const installations: string[] = [];
if (canAccess(folder)) {
// Output of the grep & print looks like:
// /opt/google/chrome/google-chrome --profile-directory
// /home/user/Downloads/chrome-linux/chrome-wrapper %U
let execPaths;

// Some systems do not support grep -R so fallback to -r.
// See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
try {
execPaths = execSync(`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`);
} catch (e) {
execPaths = execSync(`grep -Er "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`);
}

execPaths = execPaths
.toString()
.split(newLineRegex)
.map(execPath => execPath.replace(argumentsRegex, '$1'));

execPaths.forEach(execPath => canAccess(execPath) && installations.push(execPath));
}

return installations;
}

function sort(installations: string[], priorities: { regex: RegExp; weight: number }[]) {
const defaultPriority = 10;
return (
installations
// assign priorities
.map(inst => {
for (const pair of priorities) {
if (pair.regex.test(inst)) return { path: inst, weight: pair.weight };
}
return { path: inst, weight: defaultPriority };
})
// sort based on priorities
.sort((a, b) => b.weight - a.weight)
// remove priority flag
.map(pair => pair.path)
);
}

function uniq<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
}

function localPuppeteer() {
try {
require.resolve('puppeteer');
} catch {
return;
}
const p = require('puppeteer') as typeof import('puppeteer');
return p.executablePath();
}

function darwin(canary = false): string | undefined {
const LSREGISTER =
'/System/Library/Frameworks/CoreServices.framework' +
'/Versions/A/Frameworks/LaunchServices.framework' +
'/Versions/A/Support/lsregister';
const grepexpr = canary ? 'google chrome canary' : 'google chrome';
const result = execSync(`${LSREGISTER} -dump | grep -i \'${grepexpr}\\?.app$\' | awk \'{$1=""; print $0}\'`);

const paths = result
.toString()
.split(newLineRegex)
.filter(a => a)
.map(a => a.trim());
paths.unshift(canary ? '/Applications/Google Chrome Canary.app' : '/Applications/Google Chrome.app');
for (const p of paths) {
if (p.startsWith('/Volumes')) continue;
const inst = path.join(p, canary ? '/Contents/MacOS/Google Chrome Canary' : '/Contents/MacOS/Google Chrome');
if (canAccess(inst)) return inst;
}
return;
}

/**
* Look for linux executables in 3 ways
* 1. Look into CHROME_PATH env variable
* 2. Look into the directories where .desktop are saved on gnome based distro's
* 3. Look for google-chrome-stable & google-chrome executables by using the which command
*/
function linux(_canary = false) {
let installations: string[] = [];

// Look into the directories where .desktop are saved on gnome based distro's
const desktopInstallationFolders = [
path.join(require('os').homedir(), '.local/share/applications/'),
'/usr/share/applications/',
];
desktopInstallationFolders.forEach(folder => {
installations = installations.concat(findChromeExecutables(folder));
});

// Look for google-chrome(-stable) & chromium(-browser) executables by using the which command
const executables = ['google-chrome-stable', 'google-chrome', 'chromium-browser', 'chromium'];
executables.forEach(executable => {
try {
const chromePath = execFileSync('which', [executable], { stdio: 'pipe' }).toString().split(newLineRegex)[0];
if (canAccess(chromePath)) installations.push(chromePath);
} catch (e) {
// Not installed.
}
});

if (!installations.length)
throw new Error(
'The environment variable CHROME_PATH must be set to executable of a build of Chromium version 54.0 or later.',
);

const priorities = [
{ regex: /chrome-wrapper$/, weight: 51 },
{ regex: /google-chrome-stable$/, weight: 50 },
{ regex: /google-chrome$/, weight: 49 },
{ regex: /chromium-browser$/, weight: 48 },
{ regex: /chromium$/, weight: 47 },
];

if (process.env.CHROME_PATH) priorities.unshift({ regex: new RegExp(`${process.env.CHROME_PATH}`), weight: 101 });

return sort(uniq(installations.filter(Boolean)), priorities)[0];
}

function win32(canary = false) {
const suffix = canary
? `${path.sep}Google${path.sep}Chrome SxS${path.sep}Application${path.sep}chrome.exe`
: `${path.sep}Google${path.sep}Chrome${path.sep}Application${path.sep}chrome.exe`;
const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']].filter(
Boolean,
);

let result;
prefixes.forEach(prefix => {
const chromePath = path.join(prefix!, suffix);
if (canAccess(chromePath)) result = chromePath;
});
return result;
}

export type FindOptions = {
executablePath?: string;
channel: ChromeChannel;
};

export async function findChrome(options: FindOptions) {
if (options.executablePath) return { executablePath: options.executablePath, type: 'user' };

const config = new Set<ChromeChannel>([options.channel] || ['*']);

let executablePath: string | undefined = undefined;
if (config.has('puppeteer') || config.has('*')) {
executablePath = localPuppeteer();
if (executablePath) return { executablePath, type: 'puppeteer' };
}

if (config.has('canary') || config.has('*')) {
if (process.platform === 'linux') executablePath = linux(true);
else if (process.platform === 'win32') executablePath = win32(true);
else if (process.platform === 'darwin') executablePath = darwin(true);
if (executablePath) return { executablePath, type: 'canary' };
}

// Then pick stable.
if (config.has('stable') || config.has('*')) {
if (process.platform === 'linux') executablePath = linux();
else if (process.platform === 'win32') executablePath = win32();
else if (process.platform === 'darwin') executablePath = darwin();
if (executablePath) return { executablePath, type: 'stable' };
}

return null;
}
Loading

0 comments on commit 3c492d1

Please sign in to comment.