chore: don't auto-install browsers if global install (#13299)

This commit is contained in:
Ross Wollman 2022-04-08 10:46:24 -07:00 committed by GitHub
parent 10b8a8b199
commit db7bd8ebd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 423 additions and 20 deletions

View File

@ -19,3 +19,6 @@ To run all tests:
```
To install local builds of `playwright` packages in tests, do `npm_i playwright`.
Each test run will get its own npm state. You can use `local-playwright-registry <package>` to
ensure it was installed as part of the test run, and that it was a local copy.

View File

@ -0,0 +1,274 @@
#!/usr/bin/env node
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// ============================================
// See `./local-playwright-registry help` for
// usage and help.
// ============================================
const crypto = require('crypto');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
// WORK_DIR should be a local directory the Registry can work in through its lifetime. It will not be removed automatically.
const WORK_DIR = path.join(process.cwd(), '.playwright-registry');
// ACCESS_LOGS records which packages have been served locally vs. proxied to the official npm registry.
const ACCESS_LOGS = path.join(WORK_DIR, 'access.log');
// REGISTRY_URL_FILE is the URL to us to connect to the registy. It is allocated dynamically and shows up on disk only once the packages are actually available for download.
const REGISTRY_URL_FILE = path.join(WORK_DIR, 'registry.url.txt');
const kPublicNpmRegistry = 'https://registry.npmjs.org';
const kContentTypeAbbreviatedMetadata = 'application/vnd.npm.install-v1+json';
class Registry {
constructor() {
this._objectsDir = path.join(WORK_DIR, 'objects');
this._packageMeta = new Map();
this._address = '';
this._log = () => { };
}
async init() {
await fs.promises.mkdir(this._objectsDir, { recursive: true });
await fs.promises.writeFile(ACCESS_LOGS, '');
this._log = msg => fs.appendFileSync(ACCESS_LOGS, `${msg}\n`);
await Promise.all([
this._addPackage('playwright', getEnvOrDie('PLAYWRIGHT_TGZ')),
this._addPackage('playwright-core', getEnvOrDie('PLAYWRIGHT_CORE_TGZ')),
this._addPackage('playwright-chromium', getEnvOrDie('PLAYWRIGHT_CHROMIUM_TGZ')),
this._addPackage('playwright-firefox',getEnvOrDie('PLAYWRIGHT_FIREFOX_TGZ')),
this._addPackage('playwright-webkit',getEnvOrDie('PLAYWRIGHT_WEBKIT_TGZ')),
this._addPackage('@playwright/test',getEnvOrDie('PLAYWRIGHT_TEST_TGZ')),
]);
// Minimal Server that implements essential endpoints from the NPM Registry API.
// See https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md.
this._server = http.createServer(async (req, res) => {
this._log(`REQUEST: ${req.method} ${req.url}`);
// 1. Only support GET requests
if (req.method !== 'GET') {
res.writeHead(405).end();
return;
}
// 2. Determine what package is being asked for.
// The paths we can handle look like:
// - /<userSuppliedPackageName>/*/<userSuppliedTarName i.e. some *.tgz>
// - /<userSuppliedPackageName>/*
// - /<userSuppliedPackageName>
const url = new URL(req.url, kPublicNpmRegistry);
let [, userSuppliedPackageName, , userSuppliedTarName] = url.pathname.split('/');
if (userSuppliedPackageName)
userSuppliedPackageName = decodeURIComponent(userSuppliedPackageName);
if (userSuppliedTarName)
userSuppliedTarName = decodeURIComponent(userSuppliedTarName);
// 3. If we have local metadata, serve directly (otherwise, proxy to upstream).
if (this._packageMeta.has(userSuppliedPackageName)) {
const [metadata, objectPath] = this._packageMeta.get(userSuppliedPackageName);
if (userSuppliedTarName) { // Tar ball request.
if (path.basename(objectPath) !== userSuppliedTarName) {
res.writeHead(404).end();
return;
}
this._log(`LOCAL ${userSuppliedPackageName} tar`);
const fileStream = fs.createReadStream(objectPath);
fileStream.pipe(res, { end: true });
fileStream.on('error', console.error);
res.on('error', console.error);
return;
} else { // Metadata request.
this._log(`LOCAL ${userSuppliedPackageName} metadata`);
res.setHeader('content-type', kContentTypeAbbreviatedMetadata);
res.write(JSON.stringify(metadata));
res.end();
return;
}
} else { // Fall through to official registry.
this._log(`PROXIED ${userSuppliedPackageName}`);
const client = { req, res };
const toNpm = https.request({
host: url.host,
headers: { ...req.headers, 'host': url.host },
method: req.method,
path: url.pathname,
searchParams: url.searchParams,
protocol: 'https:',
}, fromNpm => {
client.res.writeHead(fromNpm.statusCode, fromNpm.statusMessage, fromNpm.headers);
fromNpm.on('error', console.error);
fromNpm.pipe(client.res, { end: true });
});
client.req.pipe(toNpm);
client.req.on('error', console.error);
return;
}
});
this._server.listen(undefined, 'localhost', () => {
this._address = new URL(`http://localhost:${this._server.address().port}`).toString();
// We now have an address to make tarball paths fully qualified.
for (const [,[metadata]] of this._packageMeta.entries()) {
for (const [,version] of Object.entries(metadata.versions))
version.dist.tarball = this._address + version.dist.tarball;
}
fs.writeFileSync(REGISTRY_URL_FILE, this._address.toString());
});
process.on('exit', () => {
console.log('closing server');
this._server.close(console.error);
});
}
async _addPackage(pkg, tarPath) {
const tmpDir = await fs.promises.mkdtemp(path.join(WORK_DIR, '.staging-package-'));
const { stderr, code } = await spawnAsync('tar', ['-xvzf', tarPath, '-C', tmpDir]);
if (!!code)
throw new Error(`Failed to untar ${pkg}: ${stderr}`);
const packageJson = JSON.parse((await fs.promises.readFile(path.join(tmpDir, 'package', 'package.json'), 'utf8')));
if (pkg !== packageJson.name)
throw new Error(`Package name mismatch: ${pkg} is called ${packageJson.name} in its package.json`);
const shasum = crypto.createHash('sha1').update(await fs.promises.readFile(tarPath)).digest().toString('hex');
const metadata = {
'dist-tags': {
latest: packageJson.version,
[packageJson.version]: packageJson.version,
},
'modified': new Date().toISOString(),
'name': pkg,
'versions': {
[packageJson.version]: {
_hasShrinkwrap: false,
name: pkg,
version: packageJson.version,
dependencies: packageJson.dependencies || {},
optionalDependencies: packageJson.optionalDependencies || {},
devDependencies: packageJson.devDependencies || {},
bundleDependencies: packageJson.bundleDependencies || {},
peerDependencies: packageJson.peerDependencies || {},
bin: packageJson.bin || {},
directories: packageJson.directories || [],
dist: {
// This needs to be updated later with a full address.
tarball: `${encodeURIComponent(pkg)}/-/${shasum}.tgz`,
shasum,
},
engines: packageJson.engines || {},
},
},
};
const object = path.join(this._objectsDir, `${shasum}.tgz`);
await fs.promises.copyFile(tarPath, object);
this._packageMeta.set(pkg, [metadata, object]);
}
static async registryFactory() {
const registry = new Registry();
await registry.init();
return registry;
}
static async waitForReady() {
const OVERALL_TIMEOUT_MS = 60000;
const registryUrl = await new Promise(async (res, rej) => {
setTimeout(rej.bind(null, new Error('Timeout: Registry failed to become ready.')), OVERALL_TIMEOUT_MS);
while (true) {
const registryUrl = await fs.promises.readFile(REGISTRY_URL_FILE, 'utf8').catch(() => null);
if (registryUrl)
return res(registryUrl);
await new Promise(r => setTimeout(r, 500));
}
});
console.log(registryUrl);
process.exit(0);
}
static async assertLocalPkg(pkg) {
const logs = await fs.promises.readFile(ACCESS_LOGS, 'utf8');
const lines = logs.split(`\n`);
if (lines.includes(`LOCAL ${pkg} metadata`) && lines.includes(`LOCAL ${pkg} tar`) && !lines.includes(`PROXIED ${pkg} metadata`))
return;
console.log('Expected LOCAL metadata and tar, and no PROXIED logs for:', pkg);
console.log('Logs:');
console.log(lines.join(`\n`));
process.exit(1);
}
}
const getEnvOrDie = varName => {
const v = process.env[varName];
if (!v)
throw new Error(`${varName} environment variable MUST be set.`);
return v;
};
const spawnAsync = (cmd, args, options) => {
const process = spawn(cmd, args, Object.assign({ windowsHide: true }, options));
return new Promise(resolve => {
let stdout = '';
let stderr = '';
if (process.stdout)
process.stdout.on('data', data => stdout += data);
if (process.stderr)
process.stderr.on('data', data => stderr += data);
process.on('close', code => resolve({ stdout, stderr, code }));
process.on('error', error => resolve({ stdout, stderr, code: 0, error }));
});
};
const commands = {
'help': async () => {
console.log(`
A minimal, inefficent npm registry to serve local npm packages, or fall through
to the offical npm registry. This is useful for testing npm and npx utilities,
but should NOT, be used for anything more.
Commands:
- help.......................: prints this help message
- start......................: starts the registry server
- wait-for-ready.............: blocks waiting for the server to print
that it's actually ready and serving the
packages you published!
- assert-downloaded <package>: confirms that <package> was served locally,
AND never proxied to the official registry.`);
},
'start': Registry.registryFactory,
'wait-for-ready': Registry.waitForReady,
'assert-served-from-local-tgz': ([pkg]) => Registry.assertLocalPkg(pkg),
};
(async () => {
const command = commands[process.argv[2]];
if (!command) {
console.log(`${process.argv[2]} not a valid command:`);
await commands['help']();
process.exit(1);
}
await command(process.argv.slice(3));
})();

View File

@ -1,19 +1,26 @@
#!/bin/bash
args=""
pkgs=""
for i in "$@"; do
if [[ "$i" == "playwright" ]]; then
args="${args} ${PLAYWRIGHT_TGZ}"
pkgs="${pkgs} playwright"
elif [[ $i == "playwright-core" ]]; then
args="${args} ${PLAYWRIGHT_CORE_TGZ}"
pkgs="${pkgs} playwright-core"
elif [[ $i == "playwright-firefox" ]]; then
args="${args} ${PLAYWRIGHT_FIREFOX_TGZ}"
pkgs="${pkgs} playwright-firefox"
elif [[ $i == "playwright-chromium" ]]; then
args="${args} ${PLAYWRIGHT_CHROMIUM_TGZ}"
pkgs="${pkgs} playwright-chromium"
elif [[ $i == "playwright-webkit" ]]; then
args="${args} ${PLAYWRIGHT_WEBKIT_TGZ}"
pkgs="${pkgs} playwright-webkit"
elif [[ $i == "@playwright/test" ]]; then
args="${args} ${PLAYWRIGHT_TEST_TGZ}"
pkgs="${pkgs} @playwright/test"
elif [[ $i == "-"* ]]; then
args="${args} $i"
else
@ -22,3 +29,29 @@ for i in "$@"; do
done
npm install $args 2>&1
SCRIPT=$(cat <<EOF
const path = require('path');
const fs = require('fs');
const packages = process.argv.slice(1);
console.log('Verifying local installation of:', packages.join(', '));
for (const package of packages) {
const expectedDir = process.env.EXPECTED_NODE_MODULES_PARENT;
const packageJsonPath = path.join(path.dirname(require.resolve(package)), 'package.json');
if (!packageJsonPath.startsWith(expectedDir)) {
console.error('Local resolution of package failed. Package:', package, 'Expected:', expectedDir, 'Got:', path.dirname(packageJsonPath));
process.exit(1);
}
const expectedVersion = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
if (expectedVersion !== process.env.PLAYWRIGHT_VERSION_UNDER_TEST) {
console.error('Version of local package did not match expectation. Package:', package, 'Expected:', expectedVersion, 'Got:', process.env.PLAYWRIGHT_VERSION_UNDER_TEST);
process.exit(1);
}
}
console.log('Confirmed local installation of:', packages.join(', '));
EOF
)
node -e "${SCRIPT}" $pkgs

View File

@ -0,0 +1,3 @@
#!/bin/bash
npx --yes playwright $@ 2>&1

View File

@ -58,6 +58,7 @@ function setup_env_variables() {
export PLAYWRIGHT_FIREFOX_TGZ="${PWD}/output/playwright-firefox.tgz"
export PLAYWRIGHT_TEST_TGZ="${PWD}/output/playwright-test.tgz"
PLAYWRIGHT_CHECKOUT="${PWD}/.."
export PLAYWRIGHT_VERSION_UNDER_TEST="$(node ${PLAYWRIGHT_CHECKOUT}/utils/workspace.js --get-version)"
}
function clean_test_root() {
@ -66,7 +67,8 @@ function clean_test_root() {
}
function initialize_test {
trap "report_test_result;cd $(pwd -P)" EXIT
TEST_TMP_NPM_SCRATCH_SPACE="$(mktemp -d)";
trap "report_test_result; kill %1; rm -rf $TEST_TMP_NPM_SCRATCH_SPACE; cd $(pwd -P);" EXIT
cd "$(dirname $0)"
# cleanup environment
@ -79,11 +81,9 @@ function initialize_test {
TEST_FILE=$(basename $0)
TEST_NAME=$(basename ${0%%.sh})
# Check if test tries to install some playwright-family package
# fron NPM registry.
# Check if test tries to install using npm directly
if grep 'npm i.*playwright' "$0" 2>&1 >/dev/null; then
# If it does, this is an error: we should always install local packages using
# the `npm_i` script.
# If it does, this is an error: you will miss output
cecho "RED" "ERROR: test tries to install playwright-family package from NPM registry!"
cecho "RED" " Do not use NPM to install playwright packages!"
cecho "RED" " Instead, use 'npm_i' command to install local package"
@ -117,7 +117,13 @@ function initialize_test {
cp "${SCRIPTS_PATH}/fixture-scripts/"* .
export PATH="${SCRIPTS_PATH}/bin:${PATH}"
# Start up our local registry and configure npm to use it
local-playwright-registry start &
export npm_config_prefix="$TEST_TMP_NPM_SCRATCH_SPACE/npm_prefix"
export npm_config_cache="$TEST_TMP_NPM_SCRATCH_SPACE/npm_cache"
export npm_config_registry="$(local-playwright-registry wait-for-ready)"
export EXPECTED_NODE_MODULES_PARENT="$(pwd -P)"
# Enable bash lines logging.
set -x
}

View File

@ -8,4 +8,4 @@ npm i -D @types/node@14
echo "import { AndroidDevice, _android, AndroidWebView, Page } from 'playwright';" > "test.ts"
echo "Running tsc"
npx -p typescript@3.7.5 tsc "test.ts"
npx --yes -p typescript@3.7.5 tsc "test.ts"

View File

@ -9,5 +9,4 @@ npm i -D @types/node@14
echo "import { Page, _electron, ElectronApplication, Electron } from 'playwright';" > "test.ts"
echo "Running tsc"
npx -p typescript@3.7.5 tsc "test.ts"
npx --yes -p typescript@3.7.5 tsc "test.ts"

View File

@ -18,4 +18,3 @@ EOF
)
# make sure all dependencies are locally installed.
node -e "${SCRIPT}"

View File

@ -0,0 +1,22 @@
#!/bin/bash
source ./initialize_test.sh && initialize_test "$@"
echo "Running codegen command without installing"
OUTPUT="$(npx_playwright codegen || true)"
local-playwright-registry assert-served-from-local-tgz playwright
if [[ "${OUTPUT}" != *'Please run the following command to download new browsers'* ]]; then
echo "ERROR: should instruct user to download browsers"
exit 1
fi
if [[ "${OUTPUT}" == *"chromium"*"downloaded"* ]]; then
echo "ERROR: should not download chromium"
exit 1
fi
if [[ "${OUTPUT}" == *"webkit"*"downloaded"* ]]; then
echo "ERROR: should not download webkit"
exit 1
fi
if [[ "${OUTPUT}" == *"firefox"*"downloaded"* ]]; then
echo "ERROR: should not download firefox"
exit 1
fi

View File

@ -0,0 +1,22 @@
#!/bin/bash
source ./initialize_test.sh && initialize_test "$@"
echo "Running global help command without first installing project"
OUTPUT="$(npx_playwright --help)"
local-playwright-registry assert-served-from-local-tgz playwright
if [[ "${OUTPUT}" == *'To avoid unexpected behavior, please install your dependencies first'* ]]; then
echo "ERROR: should not warn user about global installation"
exit 1
fi
if [[ "${OUTPUT}" == *"chromium"*"downloaded"* ]]; then
echo "ERROR: should not download chromium"
exit 1
fi
if [[ "${OUTPUT}" == *"webkit"*"downloaded"* ]]; then
echo "ERROR: should not download webkit"
exit 1
fi
if [[ "${OUTPUT}" == *"firefox"*"downloaded"* ]]; then
echo "ERROR: should not download firefox"
exit 1
fi

View File

@ -0,0 +1,26 @@
#!/bin/bash
source ./initialize_test.sh && initialize_test "$@"
echo "Running install explcitly"
OUTPUT="$(npx_playwright install || true)"
local-playwright-registry assert-served-from-local-tgz playwright
if [[ "${OUTPUT}" == *'Please run the following command to download new browsers'* ]]; then
echo "ERROR: should not tell the user to run install"
exit 1
fi
if [[ "${OUTPUT}" != *'To avoid unexpected behavior, please install your dependencies first'* ]]; then
echo "ERROR: should warn user about global installation"
exit 1
fi
if [[ "${OUTPUT}" != *"chromium"*"downloaded"* ]]; then
echo "ERROR: should download chromium"
exit 1
fi
if [[ "${OUTPUT}" != *"firefox"*"downloaded"* ]]; then
echo "ERROR: should download firefox"
exit 1
fi
if [[ "${OUTPUT}" != *"webkit"*"downloaded"* ]]; then
echo "ERROR: should download webkit"
exit 1
fi

View File

@ -3,6 +3,10 @@ source ./initialize_test.sh && initialize_test "$@"
npm_i playwright-core
OUTPUT=$(npm_i --foreground-script playwright-chromium)
if [[ "${OUTPUT}" == *'To avoid unexpected behavior, please install your dependencies first'* ]]; then
echo "ERROR: should not warn user about global installation"
exit 1
fi
if [[ "${OUTPUT}" != *"chromium"* ]]; then
echo "ERROR: should download chromium"
exit 1

View File

@ -3,6 +3,10 @@ source ./initialize_test.sh && initialize_test "$@"
npm_i playwright-core
OUTPUT=$(npm_i --foreground-script playwright-firefox)
if [[ "${OUTPUT}" == *'To avoid unexpected behavior, please install your dependencies first'* ]]; then
echo "ERROR: should not warn user about global installation"
exit 1
fi
if [[ "${OUTPUT}" == *"chromium"* ]]; then
echo "ERROR: should not download chromium"
exit 1

View File

@ -14,4 +14,3 @@ PLAYWRIGHT_BROWSERS_PATH="${BROWSERS}" npm_i playwright
# Note: the flag `--unahdnled-rejections=strict` will force node to terminate in case
# of UnhandledPromiseRejection.
PLAYWRIGHT_BROWSERS_PATH="${BROWSERS}" node --unhandled-rejections=strict node_modules/playwright/install.js

View File

@ -3,6 +3,10 @@ source ./initialize_test.sh && initialize_test "$@"
npm_i playwright-core
OUTPUT=$(npm_i --foreground-script playwright-webkit)
if [[ "${OUTPUT}" == *'To avoid unexpected behavior, please install your dependencies first'* ]]; then
echo "ERROR: should not warn user about global installation"
exit 1
fi
if [[ "${OUTPUT}" == *"chromium"* ]]; then
echo "ERROR: should not download chromium"
exit 1

View File

@ -22,8 +22,8 @@ for PKG_NAME in "playwright" \
"playwright-webkit"
do
echo "Checking types of ${PKG_NAME}"
echo "import { Page } from '${PKG_NAME}';" > "${PKG_NAME}.ts" && npx -p typescript@3.7.5 tsc "${PKG_NAME}.ts"
echo "import { Page } from '${PKG_NAME}';" > "${PKG_NAME}.ts" && npx --yes -p typescript@3.7.5 tsc "${PKG_NAME}.ts"
done;
echo "Checking types of @playwright/test"
echo npx -p typescript@3.7.5 tsc "playwright-test-types.ts"
echo npx --yes -p typescript@3.7.5 tsc "playwright-test-types.ts"

View File

@ -17,7 +17,8 @@
let install;
try {
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
if (!require('playwright-core/lib/utils').isLikelyNpxGlobal())
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
} catch (e) {
// Dev build, don't install browsers by default.
}

View File

@ -33,7 +33,7 @@ import type { BrowserType } from '../client/browserType';
import type { BrowserContextOptions, LaunchOptions } from '../client/types';
import { spawn } from 'child_process';
import { getPlaywrightVersion } from '../common/userAgent';
import { wrapInASCIIBox } from '../utils';
import { wrapInASCIIBox, isLikelyNpxGlobal } from '../utils';
import { spawnAsync } from '../utils/spawnAsync';
import { launchGridAgent } from '../grid/gridAgent';
import type { GridFactory } from '../grid/gridServer';
@ -119,8 +119,7 @@ program
.option('--with-deps', 'install system dependencies for browsers')
.option('--force', 'force reinstall of stable browser channels')
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean }) {
const isLikelyNpxGlobal = process.argv.length >= 2 && process.argv[1].includes('_npx');
if (isLikelyNpxGlobal) {
if (isLikelyNpxGlobal()) {
console.error(wrapInASCIIBox([
`WARNING: It looks like you are running 'npx playwright install' without first`,
`installing your project's dependencies.`,

View File

@ -212,3 +212,5 @@ export function streamToString(stream: stream.Readable): Promise<string> {
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
}
export const isLikelyNpxGlobal = () => process.argv.length >= 2 && process.argv[1].includes('_npx');

View File

@ -17,7 +17,8 @@
let install;
try {
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
if (!require('playwright-core/lib/utils').isLikelyNpxGlobal())
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
} catch (e) {
// Dev build, don't install browsers by default.
}

View File

@ -17,7 +17,8 @@
let install;
try {
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
if (!require('playwright-core/lib/utils').isLikelyNpxGlobal())
install = require('playwright-core/lib/server').installBrowsersForNpmInstall;
} catch (e) {
// Dev build, don't install browsers by default.
}

View File

@ -17,7 +17,8 @@
let install;
try {
install = require('playwright-core/lib/server').installDefaultBrowsersForNpmInstall;
if (!require('playwright-core/lib/utils').isLikelyNpxGlobal())
install = require('playwright-core/lib/server').installDefaultBrowsersForNpmInstall;
} catch (e) {
// Dev build, don't install browsers by default.
}