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

Pac file support #1787

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@babel/eslint-parser": "^7.14.7",
"@babel/preset-env": "^7.14.7",
"@babel/register": "^7.17.7",
"babel-plugin-transform-import-meta": "^2.2.1",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-commonjs": "^21.0.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {

// Default client API URL can be set with an env var for API development
const { PERCY_CLIENT_API_URL = 'https://percy.io/api/v1' } = process.env;
const pkg = getPackageJSON(import.meta.url);
let pkg = getPackageJSON(import.meta.url);
// minimum polling interval milliseconds
const MIN_POLLING_INTERVAL = 1_000;
const INVALID_TOKEN_ERROR_MESSAGE = 'Unable to retrieve snapshot details with write access token. Kindly use a full access token for retrieving snapshot details with Synchronous CLI.';
Expand Down Expand Up @@ -83,6 +83,8 @@ export class PercyClient {

// Stringifies client and environment info.
userAgent() {
if (this.env.forcedPkgValue) pkg = this.env.forcedPkgValue;

let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`]
.concat(`${pkg.name}/${pkg.version}`, ...this.clientInfo)
.filter(Boolean));
Expand Down
48 changes: 43 additions & 5 deletions packages/client/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ import http from 'http';
import https from 'https';
import logger from '@percy/logger';
import { stripQuotesAndSpaces } from '@percy/env/utils';
import { PacProxyAgent } from 'pac-proxy-agent';

const CRLF = '\r\n';
const STATUS_REG = /^HTTP\/1.[01] (\d*)/;

// function to create PAC proxy agent
function createPacAgent(pacUrl, options = {}) {
pacUrl = stripQuotesAndSpaces(pacUrl);
try {
const agent = new PacProxyAgent(pacUrl, {
keepAlive: true,
...options
});

logger('client:proxy').info(`Successfully loaded PAC file from: ${pacUrl}`);
return agent;
} catch (error) {
logger('client:proxy').error(`Failed to load PAC file, error message: ${error.message}, stack: ${error.stack}`);
throw new Error(`Failed to initialize PAC proxy: ${error.message}`);
}
}

// Returns true if the URL hostname matches any patterns
export function hostnameMatches(patterns, url) {
let subject = new URL(url);
Expand Down Expand Up @@ -219,11 +237,31 @@ export function proxyAgentFor(url, options) {
let { protocol, hostname } = new URL(url);
let cachekey = `${protocol}//${hostname}`;

if (!cache.has(cachekey)) {
cache.set(cachekey, protocol === 'https:'
? new ProxyHttpsAgent(options)
: new ProxyHttpAgent(options));
// If we already have a cached agent, return it
if (cache.has(cachekey)) {
return cache.get(cachekey);
}

return cache.get(cachekey);
try {
let agent;
const pacUrl = process.env.PERCY_PAC_FILE_URL;

// If PAC URL is provided, use PAC proxy
if (pacUrl) {
logger('client:proxy').info(`Using PAC file from: ${pacUrl}`);
agent = createPacAgent(pacUrl, options);
} else {
// Fall back to other proxy configuration
agent = protocol === 'https:'
? new ProxyHttpsAgent(options)
: new ProxyHttpAgent(options);
}

// Cache the created agent
cache.set(cachekey, agent);
return agent;
} catch (error) {
logger('client:proxy').error(`Failed to create proxy agent: ${error.message}`);
throw error;
}
}
7 changes: 6 additions & 1 deletion packages/client/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export function getPackageJSON(rel) {
/* istanbul ignore else: sanity check */
if (rel.startsWith('file:')) rel = url.fileURLToPath(rel);

/* istanbul ignore else: sanity check */
if (rel.startsWith('file:')) rel = url.fileURLToPath(rel);

let pkg = path.join(rel, 'package.json');
if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg));

Expand Down Expand Up @@ -135,7 +138,9 @@ export async function request(url, options = {}, callback) {
let { protocol, hostname, port, pathname, search, hash } = new URL(url);

// reference the default export so tests can mock it
let { default: http } = await import(protocol === 'https:' ? 'https' : 'http');
// bundling cli inside electron or another package fails if we import it
// like this: await import(protocol === 'https:' ? 'https' : 'http');
let { default: http } = protocol === 'https:' ? await import('https') : await import('http');
let { proxyAgentFor } = await import('./proxy.js');

// automatically stringify body content
Expand Down
29 changes: 25 additions & 4 deletions packages/core/src/api.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
import path, { dirname, resolve } from 'path';
import logger from '@percy/logger';
import { normalize } from '@percy/config/utils';
import { getPackageJSON, Server, percyAutomateRequestHandler, percyBuildEventHandler } from './utils.js';
import WebdriverUtils from '@percy/webdriver-utils';
import { handleSyncJob } from './snapshot.js';
// need require.resolve until import.meta.resolve can be transpiled
export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom');
// Previously, we used `createRequire(import.meta.url).resolve` to resolve the path to the module.
// This approach relied on `createRequire`, which is Node.js-specific and less compatible with modern ESM (ECMAScript Module) standards.
// This was leading to hard coded paths when CLI is used as a dependency in another project.
// Now, we use `fileURLToPath` and `path.resolve` to determine the absolute path in a way that's more aligned with ESM conventions.
// This change ensures better compatibility and avoids relying on Node.js-specific APIs that might cause issues in ESM environments.
import { fileURLToPath } from 'url';

const getPercyDomPath = () => {
try {
const { createRequire } = require('module');
return createRequire(import.meta.url).resolve('@percy/dom');
} catch (error) {
logger('core:server').warn([
'Failed to resolve @percy/dom path using createRequire.',
'Falling back to using fileURLToPath and path.resolve.'
].join(' '));
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
return resolve(__dirname, 'node_modules/@percy/dom');
};

// Resolved path for PERCY_DOM
export const PERCY_DOM = getPercyDomPath();

// Returns a URL encoded string of nested query params
function encodeURLSearchParams(subj, prefix) {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/findRevision.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

function download_url {
if [[ "$OS" == "Linux" ]]; then
echo "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${1}%2Fchrome-linux.zip?alt=media"
elif [[ "$OS" == "Mac" ]] || [[ "$OS" == "Mac_Arm" ]]; then
echo "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/$OS%2F${1}%2Fchrome-mac.zip?alt=media"
elif [[ "$OS" == "Win" ]] || [[ "$OS" == "Win_x64" ]]; then
echo "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/$OS%2F${1}%2Fchrome-win.zip?alt=media"
fi
}

function get_closest_rev {
while true; do
curl -I 2>/dev/null `download_url $REVISION` | head -1 | grep 404 >/dev/null
if (($? == 1)); then
break
fi
REVISION=$(($REVISION-1))
done
echo $REVISION
}


if (($# < 1)); then
printf "usage: \n"
printf " ./get_chromuim [-r] rev - will get chromium by revision\n"
exit 1
fi

export REVISION=$1

for os in "Linux" "Mac" "Mac_Arm" "Win" "Win_x64";
do
export OS=$os
echo "$OS" `get_closest_rev`
done
7 changes: 3 additions & 4 deletions packages/core/src/percy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { getProxy } from '@percy/client/utils';
import Browser from './browser.js';
import Pako from 'pako';
import {
base64encode
,
base64encode,
generatePromise,
yieldAll,
yieldTo
, redactSecrets,
yieldTo,
redactSecrets,
detectSystemProxyAndLog
} from './utils.js';

Expand Down
7 changes: 7 additions & 0 deletions packages/env/src/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ export class PercyEnv {
get token() {
return this.vars.PERCY_TOKEN || null;
}

// PERCY_FORCE_PKG_VALUE for forcing package.json values
// to be used as the current environment values in client
get forcedPkgValue() {
let pkg = this.vars.PERCY_FORCE_PKG_VALUE;
return JSON.parse(pkg) || null;
}
}

// cache getters on initial call so subsequent calls are not re-computed
Expand Down
Loading
Loading